Reference Source

src/plugins/BIMServerLoaderPlugin/BIMServerClient/bimserverclient.js

import BimServerApiPromise from './bimserverapipromise.js';
import BimServerApiWebSocket from './bimserverapiwebsocket.js';
import {geometry} from './geometry.js';
import {ifc2x3tc1} from './ifc2x3tc1.js';
import {ifc4} from './ifc4.js';
import Model from './model.js';
import {translations} from './translations_en.js';

//var XMLHttpRequest = require('node-http-xhr');

export { default as BimServerApiPromise } from './bimserverapipromise.js';
export { default as BimServerApiWebSocket } from './bimserverapiwebsocket.js';
export { default as Model } from './model.js';

//import XMLHttpRequest from 'xhr2';

// Where does this come frome? The API crashes on the absence of this
// member function?
String.prototype.firstUpper = function () {
	return this.charAt(0).toUpperCase() + this.slice(1);
};

export default class BimServerClient {
	constructor(baseUrl, notifier = null, translate = null) {
		this.interfaceMapping = {
			"ServiceInterface": "org.bimserver.ServiceInterface",
			"NewServicesInterface": "org.bimserver.NewServicesInterface",
			"AuthInterface": "org.bimserver.AuthInterface",
			"OAuthInterface": "org.bimserver.OAuthInterface",
			"SettingsInterface": "org.bimserver.SettingsInterface",
			"AdminInterface": "org.bimserver.AdminInterface",
			"PluginInterface": "org.bimserver.PluginInterface",
			"MetaInterface": "org.bimserver.MetaInterface",
			"LowLevelInterface": "org.bimserver.LowLevelInterface",
			"NotificationRegistryInterface": "org.bimserver.NotificationRegistryInterface",
		};

		// translate function override
		this.translateOverride = translate;

		// Current BIMserver token
		this.token = null;

		// Base URL of the BIMserver
		this.baseUrl = baseUrl;
		if (this.baseUrl.substring(this.baseUrl.length - 1) == "/") {
			this.baseUrl = this.baseUrl.substring(0, this.baseUrl.length - 1);
		}

		// JSON endpoint on BIMserver
		this.address = this.baseUrl + "/json";

		// Notifier, default implementation does nothing
		this.notifier = notifier;
		if (this.notifier == null) {
			this.notifier = {
				setInfo: function (message) {
					console.log("[default]", message);
				},
				setSuccess: function () {},
				setError: function () {},
				resetStatus: function () {},
				resetStatusQuick: function () {},
				clear: function () {}
			};
		}
		
		// ID -> Resolve method
		this.websocketCalls = new Map(); 

		// The websocket client
		this.webSocket = new BimServerApiWebSocket(baseUrl, this);
		this.webSocket.listener = this.processNotification.bind(this);

		// Cached user object
		this.user = null;

		// Keeps track of the unique ID's required to handle websocket calls that return something
		this.idCounter = 0;
		
		this.listeners = {};

		//    	this.autoLoginTried = false;

		// Cache for serializers, PluginClassName(String) -> Serializer
		this.serializersByPluginClassName = [];

		// Whether debugging is enabled, just a lot more logging
		this.debug = false;

		// Mapping from ChannelId -> Listener (function)
		this.binaryDataListener = {};

		// This mapping keeps track of the prototype objects per class, will be lazily popuplated by the getClass method
		this.classes = {};

		// Schema name (String) -> Schema
		this.schemas = {};
	}

	init(callback) {
		var promise = new Promise((resolve, reject) => {
			this.call("AdminInterface", "getServerInfo", {}, (serverInfo) => {
				this.version = serverInfo.version;
				//const versionString = this.version.major + "." + this.version.minor + "." + this.version.revision;
				
				this.schemas.geometry = geometry.classes;
				this.addSubtypesToSchema(this.schemas.geometry);
				
				this.schemas.ifc2x3tc1 = ifc2x3tc1.classes;
				this.addSubtypesToSchema(this.schemas.ifc2x3tc1);
				
				this.schemas.ifc4 = ifc4.classes;
				this.addSubtypesToSchema(this.schemas.ifc4);

				if (callback != null) {
					callback(this, serverInfo);
				}
				resolve(serverInfo);
			});
		});
		return promise;
	}

	addSubtypesToSchema(classes) {
		for (let typeName in classes) {
			const type = classes[typeName];
			if (type.superclasses != null) {
				type.superclasses.forEach((superClass) => {
					let directSubClasses = classes[superClass].directSubClasses;
					if (directSubClasses == null) {
						directSubClasses = [];
						classes[superClass].directSubClasses = directSubClasses;
					}
					directSubClasses.push(typeName);
				});
			}
		}
	}

	getAllSubTypes(schema, typeName, callback) {
		const type = schema[typeName];
		if (type.directSubClasses != null) {
			type.directSubClasses.forEach((subTypeName) => {
				callback(subTypeName);
				this.getAllSubTypes(schema, subTypeName, callback);
			});
		}
	}

	log(message, message2) {
		if (this.debug) {
			console.log(message, message2);
		}
	}

	translate(key) {
		if (this.translateOverride !== null) {
			return this.translateOverride(key);
		}
		key = key.toUpperCase();
		if (translations != null) {
			const translated = translations[key];
			if (translated == null) {
				console.warn("translation for " + key + " not found, using key");
				return key;
			}
			return translated;
		}
		this.error("no translations");
		return key;
	}

	login(username, password, callback, errorCallback, options) {
		if (options == null) {
			options = {};
		}
		const request = {
			username: username,
			password: password
		};
		this.call("AuthInterface", "login", request, (data) => {
			this.token = data;
			if (options.done !== false) {
				this.notifier.setInfo(this.translate("LOGIN_DONE"), 2000);
			}
			this.resolveUser(callback);
		}, errorCallback, options.busy === false ? false : true, options.done === false ? false : true, options.error === false ? false : true);
	}

	downloadViaWebsocket(msg) {
		msg.action = "download";
		msg.token = this.token;
		this.webSocket.send(msg);
	}

	setBinaryDataListener(topicId, listener) {
		this.binaryDataListener[topicId] = listener;
	}

	clearBinaryDataListener(topicId) {
		delete this.binaryDataListener[topicId];
	}
	
	processNotification(message) {
		if (message instanceof ArrayBuffer) {
			if (message == null || message.byteLength == 0) {
				return;
			}
			const view = new DataView(message, 0, 8);
			const topicId = view.getUint32(0, true) + 0x100000000 * view.getUint32(4, true); // TopicId's are of type long (64 bit)
			const listener = this.binaryDataListener[topicId];
			if (listener != null) {
				listener(message);
			} else {
				console.error("No listener for topicId", topicId);
			}
		} else {
			const intf = message["interface"];
			if (this.listeners[intf] != null) {
				if (this.listeners[intf][message.method] != null) {
					let ar = null;
					this.listeners[intf][message.method].forEach((listener) => {
						if (ar == null) {
							// Only parse the arguments once, or when there are no listeners, not even once
							ar = [];
							let i = 0;
							for (let key in message.parameters) {
								ar[i++] = message.parameters[key];
							}
						}
						listener.apply(null, ar);
					});
				} else {
					console.log("No listeners on interface " + intf + " for method " + message.method);
				}
			} else {
				console.log("No listeners for interface " + intf);
			}
		}
	}

	resolveUser(callback) {
		this.call("AuthInterface", "getLoggedInUser", {}, (data) => {
			this.user = data;
			if (callback != null) {
				callback(this.user);
			}
		});
	}

	logout(callback) {
		this.call("AuthInterface", "logout", {}, () => {
			this.notifier.setInfo(this.translate("LOGOUT_DONE"));
			callback();
		});
	}

	generateRevisionDownloadUrl(settings) {
		return this.baseUrl + "/download?token=" + this.token + (settings.zip ? "&zip=on" : "") + "&topicId=" + settings.topicId;
	}

	generateExtendedDataDownloadUrl(edid) {
		return this.baseUrl + "/download?token=" + this.token + "&action=extendeddata&edid=" + edid;
	}

	getJsonSerializer(callback) {
		this.getSerializerByPluginClassName("org.bimserver.serializers.JsonSerializerPlugin").then((serializer) => {
			callback(serializer);
		});
	}

	getJsonStreamingSerializer(callback) {
		this.getSerializerByPluginClassName("org.bimserver.serializers.JsonStreamingSerializerPlugin").then((serializer) => {
			callback(serializer);
		});
	}

	getSerializerByPluginClassName(pluginClassName) {
		if (this.serializersByPluginClassName[pluginClassName] != null) {
			return this.serializersByPluginClassName[pluginClassName];
		} else {
			var promise = new Promise((resolve, reject) => {
				this.call("PluginInterface", "getSerializerByPluginClassName", {
					pluginClassName: pluginClassName
				}, (serializer) => {
					resolve(serializer);
				});
			});

			this.serializersByPluginClassName[pluginClassName] = promise;
			
			return promise;
		}
	}

	getMessagingSerializerByPluginClassName(pluginClassName, callback) {
		if (this.serializersByPluginClassName[pluginClassName] == null) {
			this.call("PluginInterface", "getMessagingSerializerByPluginClassName", {
				pluginClassName: pluginClassName
			}, (serializer) => {
				this.serializersByPluginClassName[pluginClassName] = serializer;
				callback(serializer);
			});
		} else {
			callback(this.serializersByPluginClassName[pluginClassName]);
		}
	}

	register(interfaceName, methodName, callback, registerCallback) {
		if (callback == null) {
			throw "Cannot register null callback";
		}
		if (this.listeners[interfaceName] == null) {
			this.listeners[interfaceName] = {};
		}
		if (this.listeners[interfaceName][methodName] == null) {
			this.listeners[interfaceName][methodName] = new Set();
		}
		this.listeners[interfaceName][methodName].add(callback);
		if (registerCallback != null) {
			registerCallback();
		}
	}

	registerNewRevisionOnSpecificProjectHandler(poid, handler, callback) {
		this.register("NotificationInterface", "newRevision", handler, () => {
			this.call("NotificationRegistryInterface", "registerNewRevisionOnSpecificProjectHandler", {
				endPointId: this.webSocket.endPointId,
				poid: poid
			}, () => {
				if (callback != null) {
					callback();
				}
			});
		});
	}

	registerNewExtendedDataOnRevisionHandler(roid, handler, callback) {
		this.register("NotificationInterface", "newExtendedData", handler, () => {
			this.call("NotificationRegistryInterface", "registerNewExtendedDataOnRevisionHandler", {
				endPointId: this.webSocket.endPointId,
				roid: roid
			}, () => {
				if (callback != null) {
					callback();
				}
			});
		});
	}

	registerNewUserHandler(handler, callback) {
		this.register("NotificationInterface", "newUser", handler, () => {
			this.call("NotificationRegistryInterface", "registerNewUserHandler", {
				endPointId: this.webSocket.endPointId
			}, () => {
				if (callback != null) {
					callback();
				}
			});
		});
	}

	unregisterNewUserHandler(handler, callback) {
		this.unregister(handler);
		this.call("NotificationRegistryInterface", "unregisterNewUserHandler", {
			endPointId: this.webSocket.endPointId
		}, () => {
			if (callback != null) {
				callback();
			}
		});
	}

	unregisterChangeProgressProjectHandler(poid, newHandler, closedHandler, callback) {
		this.unregister(newHandler);
		this.unregister(closedHandler);
		this.call("NotificationRegistryInterface", "unregisterChangeProgressOnProject", {
			poid: poid,
			endPointId: this.webSocket.endPointId
		}, callback);
	}

	registerChangeProgressProjectHandler(poid, newHandler, closedHandler, callback) {
		this.register("NotificationInterface", "newProgressOnProjectTopic", newHandler, () => {
			this.register("NotificationInterface", "closedProgressOnProjectTopic", closedHandler, () => {
				this.call("NotificationRegistryInterface", "registerChangeProgressOnProject", {
					poid: poid,
					endPointId: this.webSocket.endPointId
				}, () => {
					if (callback != null) {
						callback();
					}
				});
			});
		});
	}

	unregisterChangeProgressServerHandler(newHandler, closedHandler, callback) {
		this.unregister(newHandler);
		this.unregister(closedHandler);
		if (this.webSocket.endPointId != null) {
			this.call("NotificationRegistryInterface", "unregisterChangeProgressOnServer", {
				endPointId: this.webSocket.endPointId
			}, callback);
		}
	}

	registerChangeProgressServerHandler(newHandler, closedHandler, callback) {
		this.register("NotificationInterface", "newProgressOnServerTopic", newHandler, () => {
			this.register("NotificationInterface", "closedProgressOnServerTopic", closedHandler, () => {
				this.call("NotificationRegistryInterface", "registerChangeProgressOnServer", {
					endPointId: this.webSocket.endPointId
				}, () => {
					if (callback != null) {
						callback();
					}
				});
			});
		});
	}

	unregisterChangeProgressRevisionHandler(roid, newHandler, closedHandler, callback) {
		this.unregister(newHandler);
		this.unregister(closedHandler);
		this.call("NotificationRegistryInterface", "unregisterChangeProgressOnProject", {
			roid: roid,
			endPointId: this.webSocket.endPointId
		}, callback);
	}

	registerChangeProgressRevisionHandler(poid, roid, newHandler, closedHandler, callback) {
		this.register("NotificationInterface", "newProgressOnRevisionTopic", newHandler, () => {
			this.register("NotificationInterface", "closedProgressOnRevisionTopic", closedHandler, () => {
				this.call("NotificationRegistryInterface", "registerChangeProgressOnRevision", {
					poid: poid,
					roid: roid,
					endPointId: this.webSocket.endPointId
				}, () => {
					if (callback != null) {
						callback();
					}
				});
			});
		});
	}

	registerNewProjectHandler(handler, callback) {
		this.register("NotificationInterface", "newProject", handler, () => {
			this.call("NotificationRegistryInterface", "registerNewProjectHandler", {
				endPointId: this.webSocket.endPointId
			}, () => {
				if (callback != null) {
					callback();
				}
			});
		});
	}

	unregisterNewProjectHandler(handler, callback) {
		this.unregister(handler);
		if (this.webSocket.endPointId != null) {
			this.call("NotificationRegistryInterface", "unregisterNewProjectHandler", {
				endPointId: this.webSocket.endPointId
			}, () => {
				if (callback != null) {
					callback();
				}
			}, () => {
				// Discard
			});
		}
	}

	unregisterNewRevisionOnSpecificProjectHandler(poid, handler, callback) {
		this.unregister(handler);
		this.call("NotificationRegistryInterface", "unregisterNewRevisionOnSpecificProjectHandler", {
			endPointId: this.webSocket.endPointId,
			poid: poid
		}, () => {
			if (callback != null) {
				callback();
			}
		}, () => {
			// Discard
		});
	}

	unregisterNewExtendedDataOnRevisionHandler(roid, handler, callback) {
		this.unregister(handler);
		this.call("NotificationRegistryInterface", "unregisterNewExtendedDataOnRevisionHandler", {
			endPointId: this.webSocket.endPointId,
			roid: roid
		}, () => {
			if (callback != null) {
				callback();
			}
		});
	}

	registerProgressHandler(topicId, handler, callback) {
		this.register("NotificationInterface", "progress", handler, () => {
			this.call("NotificationRegistryInterface", "registerProgressHandler", {
				topicId: topicId,
				endPointId: this.webSocket.endPointId
			}, () => {
				if (callback != null) {
					callback();
				} else {
					this.call("NotificationRegistryInterface", "getProgress", {
						topicId: topicId
					}, (state) => {
						handler(topicId, state);
					});
				}
			});
		});
	}

	unregisterProgressHandler(topicId, handler, callback) {
		this.unregister(handler);
		this.call("NotificationRegistryInterface", "unregisterProgressHandler", {
			topicId: topicId,
			endPointId: this.webSocket.endPointId
		}, () => {}).done(callback);
	}

	unregister(listener) {
		for (let i in this.listeners) {
			for (let j in this.listeners[i]) {
				const list = this.listeners[i][j];
				for (let k = 0; k < list.length; k++) {
					if (list[k] === listener) {
						list.splice(k, 1);
						return;
					}
				}
			}
		}
	}

	createRequest(interfaceName, method, data) {
		let object = {};
		object["interface"] = interfaceName;
		object.method = method;
		for (var key in data) {
			if (data[key] instanceof Set) {
				// Convert ES6 Set to an array
				data[key] = Array.from(data[key]);
			}
		}
		object.parameters = data;

		return object;
	}

	getJson(address, data, success, error) {
		const xhr = new XMLHttpRequest();
		xhr.open("POST", address);
		xhr.onerror = () => {
			if (error != null) {
				error("Unknown network error");
			}
		};
		xhr.setRequestHeader("Content-Type", "application/json; charset=UTF-8");
		xhr.onload = (jqXHR, textStatus, errorThrown) => {
			if (xhr.status === 200) {
				let data = "";
				try {
					data = JSON.parse(xhr.responseText);
				} catch (e) {
					if (e instanceof SyntaxError) {
						if (error != null) {
							error(e);
						} else {
							this.notifier.setError(e);
							console.error(e);
						}
					} else {
						console.error(e);
					}
				}
				success(data);
			} else {
				if (error != null) {
					error(jqXHR, textStatus, errorThrown);
				} else {
					this.notifier.setError(textStatus);
					console.error(jqXHR, textStatus, errorThrown);
				}
			}
		};
		xhr.send(JSON.stringify(data));
	}

	multiCall(requests, callback, errorCallback, showBusy, showDone, showError, connectWebSocket) {
		if (!this.webSocket.connected && this.token != null && connectWebSocket) {
			this.webSocket.connect().then(() => {
				this.multiCall(requests, callback, errorCallback, showBusy, showDone, showError);
			});
			return;
		}
		const promise = new BimServerApiPromise();
		let request = null;
		if (requests.length == 1) {
			request = requests[0];
			if (this.interfaceMapping[request[0]] == null) {
				this.log("Interface " + request[0] + " not found");
			}
			request = {
				request: this.createRequest(this.interfaceMapping[request[0]], request[1], request[2])
			};
		} else if (requests.length > 1) {
			let requestObjects = [];
			requests.forEach((request) => {
				if (this.interfaceMapping[request[0]] == null) {
					this.log("Interface " + request[0] + " not found");
				}
				requestObjects.push(this.createRequest(this.interfaceMapping[request[0]], request[1], request[2]));
			});
			request = {
				requests: requestObjects
			};
		} else if (requests.length === 0) {
			promise.fire();
			callback();
		}

		//    		this.notifier.clear();

		if (this.token != null) {
			request.token = this.token;
		}

		let key = requests[0][1];
		requests.forEach((item, index) => {
			if (index > 0) {
				key += "_" + item;
			}
		});

		let showedBusy = false;
		if (showBusy) {
			if (this.lastBusyTimeOut != null) {
				clearTimeout(this.lastBusyTimeOut);
				this.lastBusyTimeOut = null;
			}
			if (typeof window !== 'undefined' && window.setTimeout != null) {
				this.lastBusyTimeOut = window.setTimeout(() => {
					this.notifier.setInfo(this.translate(key + "_BUSY"), -1);
					showedBusy = true;
				}, 200);
			}
		}

		//    		this.notifier.resetStatusQuick();

		this.log("request", request);

		this.getJson(this.address, request, (data) => {
				this.log("response", data);
				let errorsToReport = [];
				if (requests.length == 1) {
					if (showBusy) {
						if (this.lastBusyTimeOut != null) {
							clearTimeout(this.lastBusyTimeOut);
						}
					}
					if (data.response.exception != null) {
						if (showError) {
							if (this.lastTimeOut != null) {
								clearTimeout(this.lastTimeOut);
							}
							this.notifier.setError(data.response.exception.message);
						} else {
							if (showedBusy) {
								this.notifier.resetStatus();
							}
						}
					} else {
						if (showDone) {
							this.notifier.setSuccess(this.translate(key + "_DONE"), 5000);
						} else {
							if (showedBusy) {
								this.notifier.resetStatus();
							}
						}
					}
				} else if (requests.length > 1) {
					data.responses.forEach((response) => {
						if (response.exception != null) {
							if (errorCallback == null) {
								this.notifier.setError(response.exception.message);
							} else {
								errorsToReport.push(response.exception);
							}
						}
					});
				}
				if (errorsToReport.length > 0) {
					errorCallback(errorsToReport);
				} else {
					if (requests.length == 1) {
						callback(data.response);
					} else if (requests.length > 1) {
						callback(data.responses);
					}
				}
				promise.fire();
			},
			(jqXHR, textStatus, errorThrown) => {
				if (textStatus == "abort") {
					// ignore
				} else {
					this.log(errorThrown);
					this.log(textStatus);
					this.log(jqXHR);
					if (this.lastTimeOut != null) {
						clearTimeout(this.lastTimeOut);
					}
					this.notifier.setError(this.translate("ERROR_REMOTE_METHOD_CALL"));
				}
				if (callback != null) {
					const result = {};
					result.error = textStatus;
					result.ok = false;
					callback(result);
				}
				promise.fire();
			});
		return promise;
	}

	getModel(poid, roid, schema, deep, callback, name) {
		const model = new Model(this, poid, roid, schema);
		if (name != null) {
			model.name = name;
		}
		model.load(deep, callback);
		return model;
	}

	createModel(poid, callback) {
		const model = new Model(this, poid);
		model.init(callback);
		return model;
	}

	callWithNoIndication(interfaceName, methodName, data, callback, errorCallback) {
		return this.call(interfaceName, methodName, data, callback, errorCallback, false, false, false);
	}

	callWithFullIndication(interfaceName, methodName, data, callback) {
		return this.call(interfaceName, methodName, data, callback, null, true, true, true);
	}

	callWithUserErrorIndication(action, data, callback) {
		return this.call(null, null, data, callback, null, false, false, true);
	}

	callWithUserErrorAndDoneIndication(action, data, callback) {
		return this.call(null, null, data, callback, null, false, true, true);
	}

	isA(schema, typeSubject, typeName) {
		let isa = false;
		if (typeSubject == typeName) {
			return true;
		}

		let subject = this.schemas[schema][typeSubject];
		if (typeSubject == "GeometryInfo" || typeSubject == "GeometryData") {
			subject = this.schemas.geometry[typeSubject];
		}

		if (subject == null) {
			console.log(typeSubject, "not found");
		}
		subject.superclasses.some((superclass) => {
			if (superclass == typeName) {
				isa = true;
				return true;
			}
			if (this.isA(schema, superclass, typeName)) {
				isa = true;
				return true;
			}
			return false;
		});
		return isa;
	}

	initiateCheckin(project, deserializerOid, callback, errorCallback) {
		this.callWithNoIndication("ServiceInterface", "initiateCheckin", {
			deserializerOid: deserializerOid,
			poid: project.oid
		}, (topicId) => {
			if (callback != null) {
				callback(topicId);
			}
		}, (error) => {
			errorCallback(error);
		});
	}

	checkin(topicId, project, comment, file, deserializerOid, progressListener, success, error) {
		const xhr = new XMLHttpRequest();

		xhr.upload.addEventListener("progress",
			(e) => {
				if (e.lengthComputable) {
					const percentage = Math.round((e.loaded * 100) / e.total);
					progressListener(percentage);
				}
			}, false);

		xhr.addEventListener("load", (event) => {
			const result = JSON.parse(xhr.response);

			if (result.exception == null) {
				if (success != null) {
					success(result.checkinid);
				}
			} else {
				if (error == null) {
					console.error(result.exception);
				} else {
					error(result.exception);
				}
			}
		}, false);

		xhr.open("POST", this.baseUrl + "/upload");

		const formData = new FormData();

		formData.append("token", this.token);
		formData.append("deserializerOid", deserializerOid);
		formData.append("comment", comment);
		formData.append("poid", project.oid);
		formData.append("topicId", topicId);
		formData.append("file", file);

		xhr.send(formData);
	}

	addExtendedData(roid, title, schema, data, success, error) {
		const reader = new FileReader();
		const xhr = new XMLHttpRequest();

		xhr.addEventListener("load", (e) => {
			const result = JSON.parse(xhr.response);

			if (result.exception == null) {
				this.call("ServiceInterface", "addExtendedDataToRevision", {
					roid: roid,
					extendedData: {
						__type: "SExtendedData",
						title: title,
						schemaId: schema.oid,
						fileId: result.fileId
					}
				}, () => {
					success(result.checkinid);
				});
			} else {
				error(result.exception);
			}
		}, false);
		xhr.open("POST", this.baseUrl + "/upload");
		if (typeof data == "File") {
			reader.onload = () => {
				const formData = new FormData();
				formData.append("action", "file");
				formData.append("token", this.token);

				const blob = new Blob([file], {
					type: schema.contentType
				});

				formData.append("file", blob, file.name);
				xhr.send(formData);
			};
			reader.readAsBinaryString(file);
		} else {
			// Assuming data is a Blob
			const formData = new FormData();
			formData.append("action", "file");
			formData.append("token", this.token);
			formData.append("file", data, data.name);
			xhr.send(formData);
		}
	}

	setToken(token, callback, errorCallback) {
		this.token = token;
		this.call("AuthInterface", "getLoggedInUser", {}, (data) => {
			this.user = data;
			this.webSocket.connect(callback);
		}, () => {
			if (errorCallback != null) {
				errorCallback();
			}
		}, true, false, true, false);
	}
	
	callWithWebsocket(interfaceName, methodName, data) {
		var promise = new Promise((resolve, reject) => {
			var id = this.idCounter++;
			this.websocketCalls.set(id, (response) => {
				resolve(response.response.result);
			});
			var request = {
				id: id,
				request: {
					interface: interfaceName,
					method: methodName,
					parameters: data
				}
			};
			if (this.token != null) {
				request.token = this.token;
			}
			this.webSocket.send(request);
		});
		return promise;
	}

	/**
	 * Call a single method, this method delegates to the multiCall method
	 * @param {string} interfaceName - Interface name, e.g. "ServiceInterface"
	 * @param {string} methodName - Methodname, e.g. "addProject"
	 * @param {Object} data - Object with a field per arument
	 * @param {Function} callback - Function to callback, first argument in callback will be the returned object
	 * @param {Function} errorCallback - Function to callback on error
	 * @param {boolean} showBusy - Whether to show busy indication
	 * @param {boolean} showDone - Whether to show done indication
	 * @param {boolean} showError - Whether to show errors
	 * 
	 */
	call(interfaceName, methodName, data, callback, errorCallback, showBusy = true, showDone = false, showError = true, connectWebSocket = true) {
		return this.multiCall([
			[
				interfaceName,
				methodName,
				data
			]
		], (data) => {
			if (data.exception == null) {
				if (callback != null) {
					callback(data.result);
				}
			} else {
				if (errorCallback != null) {
					errorCallback(data.exception);
				}
			}
		}, errorCallback, showBusy, showDone, showError, connectWebSocket);
	}
}