FHIRTreeBinding.js

sap.ui.define([
	"sap/ui/model/TreeBinding",
	"sap/ui/model/ChangeReason",
	"sap/ui/model/Sorter",
	"sap/ui/model/FilterProcessor",
	"sap/ui/model/SorterProcessor",
	"sap/fhir/model/r4/FHIRUtils",
	"sap/fhir/model/r4/OperationMode",
	"sap/ui/model/Filter",
	"sap/base/Log",
	"sap/base/util/deepEqual",
	"sap/base/util/each",
	"sap/fhir/model/r4/Context"
], function(TreeBinding, ChangeReason, Sorter, FilterProcessor, SorterProcessor, FHIRUtils, OperationMode, Filter, Log, deepEqual, each, Context) {
	"use strict";

	/**
	 * Constructor for a new FHIRTreeBinding
	 *
	 * @class
	 * @classdesc Tree binding implementation for the FHIRModel
	 * @alias sap.fhir.model.r4.FHIRTreeBinding
	 * @param {sap.fhir.model.r4.FHIRModel} oModel The FHIRModel
	 * @param {string} sPath The binding path in the model
	 * @param {sap.fhir.model.r4.Context} [oContext] The parent context which is required as base for a relative path
	 * @param {sap.ui.model.Filter | sap.ui.model.Filter[]} [aFilters] The dynamic application filters to be used initially (can be either a filter or an array of filters)
	 * @param {object} [mParameters] The map which contains additional parameters for the binding
	 * @param {string} [mParameters.groupId] The group id
	 * @param {sap.fhir.model.r4.OperationMode} [mParameters.operationMode] The operation mode, how to handle operations like filtering and sorting
	 * @param {string} [mParameters.rootSearch] The search parameter to identify the root node, e.g. 'base'
	 * @param {string} [mParameters.rootValue] The value of the search parameter to identify the root node, e.g. 'http://hl7.org/fhir/StructureDefinition/DomainResource'
	 * @param {string} [mParameters.rootProperty] The property of a FHIR resource which represents the link to the parent in the tree, e.g. 'baseDefinition'
	 * @param {string} [mParameters.nodeProperty] The property of a FHIR resource which identifies the resource as a node in the tree, e.g. 'url', Note: The `rootProperty` of a child, is the value of the `nodeProperty` of the parent
	 * @param {boolean} [mParameters.displayRootNode=false] Determines if the root node of the tree is displayed or not
	 * @param {boolean} [mParameters.collapseRecursive=true] Determines if all sub nodes of a single node will be collapsed also, if this single node is collapsed
	 * @param {number} [mParameters.numberOfExpandedLevels=0] Determines the number of levels, which will be auto-expanded initially
	 *
	 * @param {sap.ui.model.Sorter | sap.ui.model.Sorter[]} [aSorters] The dynamic sorters to be used initially (can be either a sorter or an array of sorters)
	 * @author SAP SE
	 * @extends sap.ui.model.TreeBinding
	 * @public
	 * @since 1.0.0
	 * @version 2.4.0
	*/
	var FHIRTreeBinding = TreeBinding.extend("sap.fhir.model.r4.FHIRTreeBinding", {

		constructor : function(oModel, sPath, oContext, aFilters, mParameters, aSorters) {
			TreeBinding.apply(this, arguments);
			this.aFilters = aFilters instanceof Filter ? [aFilters] : aFilters;
			this.aSorters = aSorters instanceof Sorter ? [aSorters] : aSorters;
			this.aSorters = aSorters;
			this.sId = FHIRUtils.uuidv4();
			this._checkParameters(mParameters);
			this.iExpandedNodesLength = 0;
			this.mParameters = mParameters;
			this.sRootSearch = mParameters.rootSearch;
			this.sRootProperty = mParameters.rootProperty;
			this.aRootProperty = FHIRUtils.splitPath(this.sRootProperty);
			this.sRootValue = mParameters.rootValue;
			this.sNodeProperty = mParameters.nodeProperty;
			this.aNodeProperty = FHIRUtils.splitPath(this.sNodeProperty);
			this.sOperationMode = mParameters.operationMode || this.oModel.sDefaultOperationMode;
			this.sGroupId = mParameters && mParameters.groupId || oContext && oContext.sGroupId;
			if (this.sOperationMode !== OperationMode.Server) {
				throw new Error("Unsupported OperationMode: " + this.sOperationMode + ". Only sap.fhir.model.r4.OperationMode.Server is supported.");
			}
			this.iNumberOfExpandedLevels = mParameters.numberOfExpandedLevels || 0;
			this._aRowIndexMap = [];
			this.oBindingInfo = this.oModel.getBindingInfo(this.sPath, this.oContext);

			// default value for collapse recursive
			if (this.mParameters.collapseRecursive === undefined) {
				this.bCollapseRecursive = true;
			} else {
				this.bCollapseRecursive = !!this.mParameters.collapseRecursive;
			}
			this._resetData();
		}
	});


	/**
	 * Fired, when the tree binding starts to request tree items from the FHIR server
	 *
	 * @event sap.fhir.model.r4.FHIRTreeBinding#treeLoadingStarted
	 * @param {sap.ui.base.Event} oEvent
	 * @param {sap.ui.base.EventProvider} oEvent.getSource
	 * @public
	 */

	/**
	 * Fired, when the tree binding has finished requesting tree items from the FHIR server
	 *
	 * @event sap.fhir.model.r4.FHIRTreeBinding#treeLoadingCompleted
	 * @param {sap.ui.base.Event} oEvent
	 * @param {sap.ui.base.EventProvider} oEvent.getSource
	 * @public
	 */

	/**
	 * Attach event-handler <code>fnFunction</code> to the <code>treeLoadingStarted</code> event of this <code>sap.fhir.model.r4.FHIRTreeBinding</code>.
	 *
	 * @param {object} [oData] The object, that should be passed along with the event-object when firing the event.
	 * @param {function} fnFunction The function to call, when the event occurs. This function will be called on the oListener-instance (if present) or in a 'static way'.
	 * @param {object} [oListener] Object on which to call the given function. If empty, the global context (window) is used.
	 * @returns {sap.fhir.model.r4.FHIRTreeBinding} <code>this</code> to allow method chaining
	 * @public
	 * @since 1.0.0
	 */
	FHIRTreeBinding.prototype.attachTreeLoadingStarted = function(oData, fnFunction, oListener) {
		this.attachEvent("treeLoadingStarted", oData, fnFunction, oListener);
		return this;
	};

	/**
	 * Detach event-handler <code>fnFunction</code> from the <code>treeLoadingStarted</code> event of this <code>sap.fhir.model.r4.FHIRTreeBinding</code>. The passed function and listener
	 * object must match the ones previously used for event registration.
	 *
	 * @param {function} fnFunction The function to call, when the event occurs.
	 * @param {object} oListener Object on which the given function had to be called.
	 * @returns {sap.fhir.model.r4.FHIRTreeBinding} <code>this</code> to allow method chaining
	 * @public
	 * @since 1.0.0
	 */
	FHIRTreeBinding.prototype.detachTreeLoadingStarted = function(fnFunction, oListener) {
		this.detachEvent("treeLoadingStarted", fnFunction, oListener);
		return this;
	};

	/**
	 * Fire event <code>treeLoadingStarted</code> to attached listeners.
	 *
	 * @param {any} mArguments Arguments to be fired alongside the event.
	 * @returns {sap.fhir.model.r4.FHIRTreeBinding} <code>this</code> to allow method chaining
	 * @protected
	 * @since 1.0.0
	 */
	FHIRTreeBinding.prototype.fireTreeLoadingStarted = function(mArguments) {
		this.fireEvent("treeLoadingStarted", mArguments);
		return this;
	};

	/**
	 * Attach event-handler <code>fnFunction</code> to the <code>treeLoadingCompleted</code> event of this <code>sap.fhir.model.r4.FHIRTreeBinding</code>.
	 *
	 * @param {object} [oData] The object, that should be passed along with the event-object when firing the event.
	 * @param {function} fnFunction The function to call, when the event occurs. This function will be called on the oListener-instance (if present) or in a 'static way'.
	 * @param {object} [oListener] Object on which to call the given function. If empty, the global context (window) is used.
	 * @returns {sap.fhir.model.r4.FHIRTreeBinding} <code>this</code> to allow method chaining
	 * @public
	 * @since 1.0.0
	 */
	FHIRTreeBinding.prototype.attachTreeLoadingCompleted = function(oData, fnFunction, oListener) {
		this.attachEvent("treeLoadingCompleted", oData, fnFunction, oListener);
		return this;
	};

	/**
	 * Detach event-handler <code>fnFunction</code> from the <code>treeLoadingCompleted</code> event of this <code>sap.fhir.model.r4.FHIRTreeBinding</code>. The passed function and listener
	 * object must match the ones previously used for event registration.
	 *
	 * @param {function} fnFunction The function to call, when the event occurs.
	 * @param {object} oListener Object on which the given function had to be called.
	 * @returns {sap.fhir.model.r4.FHIRTreeBinding} <code>this</code> to allow method chaining
	 * @public
	 * @since 1.0.0
	 */
	FHIRTreeBinding.prototype.detachTreeLoadingCompleted = function(fnFunction, oListener) {
		this.detachEvent("treeLoadingCompleted", fnFunction, oListener);
		return this;
	};

	/**
	 * Fire event <code>treeLoadingCompleted</code> to attached listeners.
	 * @param {any} mArguments Arguments to be fired alongside the event.
	 * @returns {sap.fhir.model.r4.FHIRTreeBinding} <code>this</code> to allow method chaining
	 * @protected
	 * @since 1.0.0
	 */
	FHIRTreeBinding.prototype.fireTreeLoadingCompleted = function(mArguments) {
		this.fireEvent("treeLoadingCompleted", mArguments);
		return this;
	};

	/**
	 * Checks if rootSearch, rootProperty, rootValue and nodeProperty are set in <code>mParameters</code>
	 * @param {object} mParameters Parameters to check.
	 * @private
	 * @since 1.0.0
	 */
	FHIRTreeBinding.prototype._checkParameters = function(mParameters) {
		if (!mParameters || !FHIRUtils.isObject(mParameters) || FHIRUtils.isEmptyObject(mParameters)) {
			throw new Error("Missing parameters: rootSearch, rootProperty, rootValue and nodeProperty have to be set in parameters.");
		}
		FHIRUtils.checkFHIRSearchParameter(mParameters, "rootSearch");
		FHIRUtils.checkPathParameter(mParameters, "rootProperty");
		FHIRUtils.checkStringParameter(mParameters, "rootValue");
		FHIRUtils.checkPathParameter(mParameters, "nodeProperty");
	};

	/**
	 * Returns already created binding contexts for all entities in this tree binding for the range determined by the given start index <code>iStart</code> and <code>iLength</code>. Resource
	 * profiles which are mentioned in the context but aren't loaded already, are requested
	 *
	 * @param {number} [iStartIndex] The index where to start the retrieval of contexts
	 * @param {number} [iLength] The number of contexts to retrieve beginning from the start index
	 * @param {number} iThreshold The threshold for the tree table
	 * @param {number} bReturnNodes The flag if the method should return the nodes for the tree table
	 * @returns {sap.fhir.model.r4.Context[]} The array of all binding contexts
	 * @protected
	 * @since 1.0.0
	 */
	FHIRTreeBinding.prototype.getContexts =
			function(iStartIndex, iLength, iThreshold, bReturnNodes) {
				if (!this.iLength && iLength !== undefined){
					this.iLength = iLength;
				} else if (!this.iLength) {
					this.iLength = this.oModel.iSizeLimit;
				}
				var mParameters = this._buildParameters(this.iLength);
				this._buildContexts();
				this._createRootNode(iStartIndex, this.iLength);

				if (!this.sNextLink && !this.bPendingRequest && this.aFilters && FHIRUtils.isEmptyObject(this.mRequestHandle)) {
					this.aKeys = undefined;
					var aRootChildItems = [];
					this._buildFilteredTree(aRootChildItems);
					this._mTreeStateOld = FHIRUtils.deepClone(this._mTreeState);
					if (this.aKeys) {
						this.iTotalLength = this.aKeys.length;
					}
					this._buildContexts();
				} else if (!this.bPendingRequest && !this.aFilters) {
					this._buildTree();
				}


				var fnSuccess = function(oData) {
					if (oData.total === undefined){
						throw new Error("FHIR Server error: The \"total\" property is missing in the response for the requested FHIR resource " + this.sPath);
					}
					this.bDirectCallPending = false;
					var oBindingInfo = this.oModel.getBindingInfo(this.sPath, this.oContext);
					this.bInitial = false;
					if (oData.entry) {
						if (this.aFilters) {
							if (Object.keys(this.mRequestHandle).length === 0) {
								this._handlePaging(oData);
								this.bPendingRequest = false;
							}
							each(oData.entry, function(i, oEntry) {
								if (oBindingInfo.getResourceType() === oEntry.resource.resourceType) {
									this._mFilteredTreeItems[oEntry.resource.id] = oEntry.resource;
								}
							}.bind(this));
							if (!this.sNextLink) {
								var oRequestHandle;
								var mParams;
								for ( var sKey in this._mFilteredTreeItems) {
									if (this._mFilteredTreeItems.hasOwnProperty(sKey)) {
										var sRootValue = this.oModel._getProperty(this._mFilteredTreeItems[sKey], this.aRootProperty);
										var sNodeValue = this.oModel._getProperty(this._mFilteredTreeItems[sKey], this.aNodeProperty);
										if (sRootValue && sRootValue !== this.sRootValue && sNodeValue !== this.sRootValue) {
											var aItems = [];
											FHIRUtils.filterObject(this._mFilteredTreeItems, this.sNodeProperty, sRootValue, 1 + FHIRUtils.getNumberOfLevelsByPath(this.sNodeProperty), aItems);
											if (aItems.length === 0) {
												mParams = { urlParameters : {}};
												mParams.urlParameters[this.sNodeProperty + ":exact"] = sRootValue;
												oRequestHandle = this._submitRequest(this.sPath, mParams, fnSuccess);
												this.mRequestHandle[oRequestHandle.getId()] = oRequestHandle;
												oRequestHandle.getRequest().always(function(oGivenRequestHandle) {
													delete this.mRequestHandle[oGivenRequestHandle.getId()];
													this._canRootAggregationsBeResolved(oData);
												}.bind(this, oRequestHandle));
												break;
											}
										}
									}
								}
							}
						} else {
							var oCurrentSection = this._oRootNode.nodeState.sections[0];
							if (!this.aKeys) {
								this.aKeys = [];
								iStartIndex = 0;
								oCurrentSection.startIndex = 0;
								oCurrentSection.length = oData.entry.length;
							} else {
								iStartIndex = this.aKeys.length;
								oCurrentSection.startIndex += oData.entry.length;
								oCurrentSection.length += oData.entry.length;
							}
							each(oData.entry, function(i, oEntry) {
								if (oBindingInfo.getResourceType() === oEntry.resource.resourceType) {
									this.aKeys[iStartIndex + i] = oEntry.resource.resourceType + "/" + oEntry.resource.id;
								}
							}.bind(this));

							this._markSuccessRequest(oData, oData.total);
						}
					} else {
						this._markSuccessRequest(oData, oData.total);
					}
				}.bind(this);

				// retrieve the requested section of nodes from the tree
				var aNodes = [];
				if (this._oRootNode) {
					aNodes = this._retrieveNodeSection(this._oRootNode, iStartIndex, this.iLength);
				}

				// keep a map between Table.RowIndex and tree nodes
				this._updateRowIndexMap(aNodes, iStartIndex);

				if (!this.bPendingRequest){
					if (!this.aSortersCache && !this.aFilterCache && this.sNextLink && (!bReturnNodes || iStartIndex > this.iStartIndex)) {
						this.iStartIndex += this.iLength;
						this._callNextLink(fnSuccess);
					} else if (this.iTotalLength === undefined){
						this.iStartIndex = 0;
						this._submitRequest(this.sPath, mParameters, fnSuccess);
					} else {
						this.bTreeLoadingStartedFired = false;
						this.fireTreeLoadingCompleted();
					}
				}

				if (bReturnNodes){
					return aNodes;
				} else {
					return this.aContexts;
				}
			};

	/**
	* Retrieves the "Lead-Selection-Index"
	* Normally this is the last selected node/table row.
	* @returns {number} returns the lead selection index or -1 if none is set
	* @protected
	* @since 1.0.0
	*/
	FHIRTreeBinding.prototype.getSelectedIndex = function () {
		//if we have no nodes selected, the lead selection index is -1
		if (!this._sLeadSelectionGroupID || FHIRUtils.isEmptyObject(this._mTreeState.selected)) {
			return -1;
		}

		// find the first selected entry -> this is our lead selection index
		var iNodeCounter = -1;
		var fnMatchFunction = function (oNode) {
			if (!oNode || !oNode.isArtificial) {
				iNodeCounter++;
			}

			if (oNode) {
				if (oNode.groupID === this._sLeadSelectionGroupID) {
					return true;
				}
			}
			return undefined;
		};
		this._match(this._oRootNode, [], 1, fnMatchFunction);

		return iNodeCounter;
	};

	/**
	 * Returns the context of node when it was found
	 * @param {number} iIndex The index of a node in the tree
	 * @returns {object} the context of a node
	 * @protected
	 * @since 1.0.0
	 */
	FHIRTreeBinding.prototype.getContextByIndex = function (iIndex) {
		if (this.isInitial()) {
			return undefined;
		}

		var oNode = this.findNode(iIndex);
		return oNode !== undefined ? oNode.context : undefined;
	};

	/**
	 * Calls the next link of paging
	 *
	 * @param {function} [fnSuccess] the call back after next link was called
	 * @private
	 * @since 1.0.0
	 */
	FHIRTreeBinding.prototype._callNextLink = function(fnSuccess) {
		if (FHIRUtils.isEmptyObject(this.mRequestHandle)) {
			var oRequestHandle = this._submitRequest(this.sNextLink, undefined, fnSuccess, undefined, true);
			this.mRequestHandle[oRequestHandle.getId()] = oRequestHandle;
			oRequestHandle.getRequest().always(function(oGivenRequestHandle) {
				delete this.mRequestHandle[oGivenRequestHandle.getId()];
			}.bind(this, oRequestHandle));
		}
	};

	/**
	 * Builds the filtered tree from the given node and their children
	 *
	 * @param {object} [aRootChildItems] contains all children of the node
	 * @param {function} [oNode] to build the context and organize child relations
	 * @private
	 * @since 1.0.0
	 */
	FHIRTreeBinding.prototype._buildFilteredTree =
			function(aRootChildItems, oNode) {
				if (oNode && aRootChildItems.length > 0) {
					var aChildItems;
					for (var i = 0; i < aRootChildItems.length; i++) {
						aChildItems = [];
						FHIRUtils.filterObject(this._mFilteredTreeItems, this.sRootProperty, aRootChildItems[i].object[this.sNodeProperty], 1 + FHIRUtils.getNumberOfLevelsByPath(this.sNodeProperty),
							aChildItems, function(oObject) {
								return {
									children : undefined,
									object : oObject
								};
							});
						aRootChildItems[i].children = aChildItems;
						var oCurrentSection = oNode.nodeState.sections[0];
						oCurrentSection.startIndex = 0;
						oCurrentSection.length = aRootChildItems.length;
						var oChildNode = this._buildFilteredChildContexts(aRootChildItems[i], i, oNode);
						if (aChildItems.length !== 0) {
							this._buildFilteredTree(aChildItems, oChildNode);
						}
					}
				} else if (!this.aKeys && !FHIRUtils.isEmptyObject(this._mFilteredTreeItems)) {
					FHIRUtils.filterObject(this._mFilteredTreeItems, this.sRootProperty, this.sRootValue, 1 + FHIRUtils.getNumberOfLevelsByPath(this.sNodeProperty), aRootChildItems,
						function(oObject) {
							return {
								children : undefined,
								object : oObject
							};
						});
					this._buildFilteredTree(aRootChildItems, this._oRootNode);
				}
			};
	/**
	 * Builds the contexts for the given node
	 *
	 * @param {object} [oResource] contains all children of the node
	 * @param {number} [i] index where to insert the child in the tree
	 * @param {object} oNode to build the contexts and organize child relations
	 * @returns {undefined | object} Child node if oNode is expanded, otherwise undefined.
	 * @private
	 * @since 1.0.0
	 */
	FHIRTreeBinding.prototype._buildFilteredChildContexts = function(oResource, i, oNode) {
		if (oNode.nodeState.expanded) {
			var iStartIndex;
			if (!this.aKeys) {
				this.aKeys = [];
				iStartIndex = 0;
			} else {
				iStartIndex = this.aKeys.length;
			}
			this.aKeys[iStartIndex] = oResource.object.resourceType + "/" + oResource.object.id;
			this._buildContexts();
			var aChildContexts = this.aContexts.splice(iStartIndex, this.aKeys.length);
			var oChildNode = this._processChildren(oNode, aChildContexts[0], i, {
				total : oResource.children.length,
				expanded : true
			});
			return oChildNode;
		}
		return undefined;
	};

	/**
	 * Checks if the root aggregation can be resolved when there are no more pending requests in the queue and builds after that the filtered tree
	 *
	 * @private
	 * @since 1.0.0
	 */
	FHIRTreeBinding.prototype._canRootAggregationsBeResolved = function() {
		if (FHIRUtils.isEmptyObject(this.mRequestHandle)) {
			this.bPendingRequest = false;
			var aRootChildItems = [];
			this._buildFilteredTree(aRootChildItems);
			this.iTotalLength = this.aKeys.length;
			this._fireChange({
				reason : ChangeReason.Change
			});
		}
	};

	/**
	 * @typedef {object} sap.fhir.model.r4.FHIRTreeBinding.Parameter
	 * @prop {object} [urlParameters] The parameters that will be passed as query strings
	 * @public
	 * @since 1.0.0
	 */

	/**
	 * Creates the parameters for the FHIR request based on the configured filters and sorters
	 *
	 * @param {number} [iLength] The number of contexts to retrieve beginning from the start index
	 * @returns {sap.fhir.model.r4.FHIRTreeBinding.Parameter} The map of parameters
	 * @private
	 * @since 1.0.0
	 */
	FHIRTreeBinding.prototype._buildParameters = function(iLength) {
		var mParameters = {
			urlParameters : {
				_sort : FHIRUtils.createSortParams(this.aSorters)
			}
		};

		FHIRUtils.addRequestQueryParameters(this, mParameters);

		if (this.aFilters) {
			FHIRUtils.filterBuilder(this.aFilters, mParameters.urlParameters, this.oModel.iSupportedFilterDepth);
		} else {
			mParameters.urlParameters[this.sRootSearch] = this.sRootValue;
			mParameters.urlParameters._count = iLength;
		}
		return mParameters;
	};

	/**
	 * Writes the previous and next link in instance variables that can be reused
	 *
	 * @param {object} oData meta information of the response about the next previous link
	 * @private
	 * @since 1.0.0
	 */
	FHIRTreeBinding.prototype._handlePaging = function(oData) {
		if (oData && oData.link) {
			this.sNextLink = FHIRUtils.getLinkUrl(oData.link, "next");
			this.sPrevLink = FHIRUtils.getLinkUrl(oData.link, "previous");
		}
	};
	/**
	 * Mark the pending request as successful and attach a new data received callback
	 *
	 * @param {object} oData The data retrieved from the server
	 * @param {number} iTotalLength The number of resources retrieved from the server
	 * @private
	 * @since 1.0.0
	 */
	FHIRTreeBinding.prototype._markSuccessRequest = function(oData, iTotalLength) {
		this._handlePaging(oData);
		if (this.iTotalLength === undefined){
			this.iTotalLength = iTotalLength;
		}
		this.bPendingRequest = false;
		this.oModel.attachAfterUpdate(function() {
			this.fireDataReceived({
				data : oData
			});
		}.bind(this));
	};

	/**
	 * Determines the number of entities contained by the actual tree binding
	 *
	 * @returns {number} The number of entities contained by the current list binding
	 * @public
	 * @since 1.0.0
	 */
	FHIRTreeBinding.prototype.getLength = function() {
		return this.iTotalLength;
	};

	/**
	 * Refreshes the tree binding
	 *
	 * @param {sap.ui.model.ChangeReason} sChangeReason The reason for refreshing the binding
	 * @public
	 * @since 1.0.0
	 */
	FHIRTreeBinding.prototype.refresh = function(sChangeReason) {
		this._resetData();
		this._fireChange({
			reason : sChangeReason
		});
	};

	/**
	 * Resets the data of the tree binding
	 *
	 * @private
	 * @since 1.0.0
	 */
	FHIRTreeBinding.prototype._resetData = function() {
		for (var key in this.mRequestHandle){
			this.mRequestHandle[key].getRequest().abort();
		}
		this.mRequestHandle = {};
		this.aKeys = undefined;
		this.aContexts = undefined;
		this.iTotalLength = undefined;
		this.bInitial = true;
		this.sNextLink = undefined;
		this.sPrevLink = undefined;
		this._oRootNode = undefined;
		this._mTreeState = undefined;
		this._mTreeStateOld = undefined;
		this.bPendingRequest = false;
		this.iLength = undefined;
		this.bTreeLoadingStartedFired = false;
		this.aSortersCache = undefined;
		this.aFilterCache = undefined;
		this._mFilteredTreeItems = {};
		this._createTreeState();
		this.iExpandedNodesLength = 0;
	};

	/**
	 * Determines if the list binding is configured in client mode
	 *
	 * @returns {boolean} if the model is initialized with the client mode
	 * @private
	 * @since 1.0.0
	 */
	FHIRTreeBinding.prototype._isClientMode = function() {
		return this.sOperationMode === OperationMode.Client;
	};

	/**
	 * Determines if the list binding is configured in server mode
	 *
	 * @returns {boolean} if the model is initialized with the server mode
	 * @private
	 * @since 1.0.0
	 */
	FHIRTreeBinding.prototype._isServerMode = function() {
		return this.sOperationMode === OperationMode.Server;
	};

	/**
	 * Filters the actual list binding depending on the given <code>aFilters</code>
	 *
	 * @param {sap.ui.model.Filter[]} [aFilters] The filters defined for the list binding
	 * @param {sap.ui.model.FilterType} sFilterType Type of the filter which should be adjusted, if it is not given, the standard behaviour applies
	 * @public
	 * @since 1.0.0
	 */
	FHIRTreeBinding.prototype.filter = function (aFilters, sFilterType) {
		FHIRUtils.filter(aFilters, this, sFilterType);
	};

	/**
	 * Sorts the actual list binding based on the given <code>aSorters</code>
	 *
	 * @param {sap.ui.model.Sorter[]} aSorters The sorters defined for the list binding
	 * @param {boolean} bRefresh If the binding should directly send a call or wait for the filters, for p13ndialog
	 * @public
	 * @since 1.0.0
	 */
	FHIRTreeBinding.prototype.sort = function(aSorters, bRefresh) {
		FHIRUtils.sort(aSorters, this, bRefresh);
	};

	/**
	 * Sets the context of the list binding and refreshes the binding
	 *
	 * @param {sap.fhir.model.r4.Context} oContext The context object
	 * @protected
	 * @since 1.0.0
	 */
	FHIRTreeBinding.prototype.setContext = function(oContext) {
		if (this.oContext !== oContext && this.isRelative()) {
			this.oContext = oContext;
			this._resetData();
		}
	};

	/**
	 * Executes an ajax call with given <code>sPath</code>, <code>mParameters</code>. Additionally, it's possible to assign the given function <code>fnSuccessCallback</code> as a callback
	 * function which is executed when the ajax call was executed successfully
	 *
	 * @param {string} sPath The path of the resource which will be requested, relative to the root URL of the FHIR server
	 * @param {object} mParameters The URL parameters which are send by the request e.g. _count, _summary
	 * @param {function} fnSuccessCallbackBeforeMapping The callback function which is executed if the request was successful
	 * @param {function} fnSuccessCallbackAfterMapping The callback which is executed after the mapping of the data fCallback
	 * @param {boolean} [bForceDirectCall] Determines if this binding should avoid the bundle request
	 * @returns {sap.fhir.model.r4.lib.RequestHandle} A request handle.
	 * @private
	 * @since 1.0.0
	 */
	FHIRTreeBinding.prototype._submitRequest = function(sPath, mParameters, fnSuccessCallbackBeforeMapping, fnSuccessCallbackAfterMapping, bForceDirectCall) {
		this.bPendingRequest = true;
		var fnErrorCallback = function() {
			this.bPendingRequest = false;
			this.bInitial = false;
		}.bind(this);
		this.bDirectCallPending = bForceDirectCall;
		if (!mParameters){
			mParameters = {};
		}
		mParameters.binding = this;
		mParameters.forceDirectCall = bForceDirectCall;
		mParameters.successBeforeMapping = fnSuccessCallbackBeforeMapping;
		mParameters.success = fnSuccessCallbackAfterMapping;
		mParameters.error = fnErrorCallback;
		var oRequestHandle = this.oModel.loadData(sPath, mParameters);
		this.bPendingRequest = true;
		if (!this.bTreeLoadingStartedFired) {
			this.bTreeLoadingStartedFired = true;
			this.fireTreeLoadingStarted();
		}
		return oRequestHandle;
	};

	/**
	 * Builds the binding context array for the list
	 *
	 * @private
	 * @since 1.0.0
	 */
	FHIRTreeBinding.prototype._buildContexts = function() {
		if (!this.aContexts) {
			this.aContexts = [];
		}
		if (this.aKeys) {
			this.aContexts = [];
			for (var j = 0; j < this.aKeys.length; j++) {
				this.aContexts.push(Context.create(this.oModel, this, "/" + this.aKeys[j], this.sGroupId));
			}
		}
	};

	/**
	 * Returns a node by the given index
	 *
	 * @param {number} iIndex The index of the node
	 * @returns {object} Node
	 * @protected
	 * @since 1.0.0
	 */
	FHIRTreeBinding.prototype.getNodeByIndex = function(iIndex) {
		if (this.bInitial) {
			return undefined;
		}

		// if the requested index is bigger than the magnitude of the tree, the index can never
		// be inside the tree.
		if (iIndex >= this.getLength()) {
			return undefined;
		}

		return this.findNode(iIndex);
	};

	/**
	 * Find node retrieves an actual tree nodes. However if there are sum rows cached (meaning, they are currently displayed), these will also be returned.
	 * @param {any} vParam Node to find.
	 * @returns {object | undefined} The found node or undefined.
	 * @protected
	 * @since 1.0.0
	 */
	FHIRTreeBinding.prototype.findNode = function(vParam) {
		if (this.bInitial) {
			return undefined;
		}

		var sParameterType = typeof vParam;
		var oFoundNode;

		var aSearchResult = [];

		// if the parameter is an index -> first check the cache, and then search the tree if necessary
		if (sParameterType === "number") {
			oFoundNode = this._aRowIndexMap[vParam];

			if (!oFoundNode) {
				var iIndexCounter = -1;
				this._match(this._oRootNode, aSearchResult, 1, function() {
					if (iIndexCounter === vParam) {
						return true;
					}
					iIndexCounter += 1;
					return undefined;
				});

				oFoundNode = aSearchResult[0];
			}

		}
		/*
		 * else if (sParameterType === "string" || sParameterType === "object") { // match auf group id // oFoundNode = aSearchResult[0]; }
		 */

		return oFoundNode;
	};

	/**
	 * Calls the given matching function on every child node in the sub tree with root "oNode". The matching function must
	 * return "true" if the node should be collected as a match, and false otherwise.
	 *
	 * @param {object} oNode the starting node of the sub-tree which will be traversed, handed to the fnMatchFunction
	 * @param {array} aResults the collected nodes for which the matching function returns true
	 * @param {number} iMaxNumberOfMatches the maximum number of matched nodes, _match() will stopp if this boundary is reached
	 * @param {function} fnMatchFunction the match function is called for every traversed nodes
	 * @param {number} [iPositionInParent] the relative position of the oNode parameter to its parent nodes children array, handed to the fnMatchFunction
	 * @param {object} [oParentNode] the parent node of the oNode parameter, handed to the fnMatchFunction
	 * @returns {boolean} if generally a math was found
	 * @private
	 * @since 1.0.0
	 */
	FHIRTreeBinding.prototype._match = function(oNode, aResults, iMaxNumberOfMatches, fnMatchFunction, iPositionInParent, oParentNode) {
		// recursion end if max number of matches have been collected
		// if iMaxNumberOfMatches is undefined -> the whole tree is searched.
		if (aResults.length === iMaxNumberOfMatches) {
			return true;
		}

		// push the node if it matches the criterium
		var bNodeMatches = fnMatchFunction.call(this, oNode, iPositionInParent, oParentNode);
		if (bNodeMatches) {
			aResults.push(oNode);
		}

		// if the node is not defined: there is a missing section in our tree
		if (!oNode) {
			return false;
		}

		if (oNode.nodeState.expanded){
			for (var i = 0; i < oNode.children.length; i++) {
				var oChildNode = oNode.children[i];
				var bMaxNumberReached = this._match(oChildNode, aResults, iMaxNumberOfMatches, fnMatchFunction, i, oNode);
				// break recursion if enough nodes where collected
				if (bMaxNumberReached) {
					return true;
				}
			}
		}

		// check if an after match hook is defined on sub-adapters
		return false;
	};

	/**
	 * Depth-First traversal of a sub-tree object structure starting with the given node as the root. Retrieves all found nodes (including gaps). Gaps will be filled with placeholder nodes. These
	 * placeholders are later used to automatically update the tree after invalidating and refreshing the sub-tree(s) containing the gaps.
	 *
	 * @param {object} oNode the root node of the sub-tree for which the section will be retrieved
	 * @param {number} iStartIndex the start of the tree section which should be retrieved
	 * @param {number} iLength the end of the tree section which should be retrieved
	 * @returns {object[]} an array containing all collected nodes, for which the absolute node index is greater than iStartIndex the length of the array will be iLength (or less if the tree does not
	 *         have that many nodes).
	 * @private
	 * @since 1.0.0
	 */
	FHIRTreeBinding.prototype._retrieveNodeSection = function(oNode, iStartIndex, iLength) {
		var iNodeCounter = -1;
		var aNodes = [];

		this._match(oNode, [], iLength, function(oToBeMatchedNode) {
			// make sure to exclude the artificial root node from being counted
			if (!oToBeMatchedNode || !oToBeMatchedNode.isArtificial) {
				iNodeCounter++;
			}
			if (iNodeCounter >= iStartIndex && oToBeMatchedNode && this.aContexts.indexOf(oToBeMatchedNode.context) > -1) {
				aNodes.push(oToBeMatchedNode);
				return true;
			}
			return false;
		});

		return aNodes;
	};

	/**
	 * Creates the root node with the amount of children given by the startIndex and the endIndex
	 *
	 * @param {number} iStartIndex the start of the tree section
	 * @param {number} iLength the end of the tree section
	 * @private
	 * @since 1.0.0
	 */
	FHIRTreeBinding.prototype._createRootNode = function(iStartIndex, iLength) {
		if (!this._oRootNode) {
			// create the root node
			var sRootGroupID = this._calculateGroupID({
				context : null,
				parent : null
			});

			var oRootNodeState = this._getNodeState(sRootGroupID);

			// create root node state if none exists
			if (!oRootNodeState) {
				oRootNodeState = this._createNodeState({
					groupID : sRootGroupID,
					sum : true,
					sections : [
						{
							startIndex : iStartIndex,
							length : iLength
						}
					]
				});

				// the root node is expanded by default under the following conditions:
				// 1: root node is artifical/should not be displayed OR we have an autoExpand situation (numberOfExpandedLevels > 0)
				// 2: the root node was not previously collapsed by the user
				this._updateTreeState({
					groupID : oRootNodeState.groupID,
					fallbackNodeState : oRootNodeState,
					expanded : true
				});
			}

			this._oRootNode = this._createNode({
				context : null,
				parent : null,
				level : this.bDisplayRootNode && this.oRootContext ? 0 : -1,
				nodeState : oRootNodeState,
				isLeaf : false,
				autoExpand : this.getNumberOfExpandedLevels() + 1
			});

			// flag the root node as artificial in case we have no real root context (but only children)
			this._oRootNode.isArtificial = true;
			this.vNodeLastInteraction = this._oRootNode;
		}
	};

	/**
	 * Builds the tree either in the initial case from the root node or when user did an interaction from the last node interaction
	 *
	 * @param {number} iStartIndex the start of the tree section
	 * @param {number} iLength the end of the tree section
	 * @private
	 * @since 1.0.0
	 */
	FHIRTreeBinding.prototype._buildTree = function() {
		// expanded the root node if requested
		if (this._mTreeState.expanded[this._oRootNode.groupID]) {
			if (Array.isArray(this.vNodeLastInteraction)){
				for (var i = 0; i < this.vNodeLastInteraction.length; i++){
					this._loadChildContexts(this.vNodeLastInteraction[i]);
				}
			} else {
				this._loadChildContexts(this.vNodeLastInteraction  || this._oRootNode);
			}
			this.vNodeLastInteraction = this._oRootNode;
		}
	};

	/**
	 * Synchronize a node section from the tree with our RowIndex Mapping table, that the indention is correct
	 *
	 * @param {object} aNodes the nodes which shall be indented
	 * @param {number} iStartIndex from where the indentions have to be handled
	 * @private
	 * @since 1.0.0
	 */
	FHIRTreeBinding.prototype._updateRowIndexMap = function(aNodes, iStartIndex) {
		// throw away the old mapping index
		this._aRowIndexMap = [];

		for (var i = 0; i < aNodes.length; i++) {
			this._aRowIndexMap[iStartIndex + i] = aNodes[i];
		}
	};

	/**
	 * Creates a new tree node with valid default values
	 *
	 * @param {object} mParameters a set of parameters which might differ from the default values
	 * @returns {object} a newly created tree node
	 * @private
	 * @since 1.0.0
	 */
	FHIRTreeBinding.prototype._createNode = function(mParameters) {
		mParameters = mParameters || {};
		var oContext = mParameters.context;
		var oNode = {
			context : oContext,
			level : mParameters.level || 0,
			children : mParameters.children || [],
			parent : mParameters.parent,
			nodeState : mParameters.nodeState,
			isLeaf : mParameters.isLeaf,
			// the relative position of the node inside its parents children array
			positionInParent : mParameters.positionInParent,
			// the sum of all child nodes in the sub-tree (below this node)
			magnitude : mParameters.magnitude || 0,
			// the total number of sum rows in the sub-tree
			numberOfTotals : mParameters.numberOfTotals || 0,
			// the total number of leafs in the sub-tree
			numberOfLeafs : mParameters.numberOfLeafs || 0,
			autoExpand : mParameters.autoExpand || 0,
			absoluteNodeIndex : mParameters.absoluteNodeIndex || 0,
			totalNumberOfLeafs : 0
		};
		// calculate the group id
		if (oContext !== undefined) {
			oNode.groupID = this._calculateGroupID(oNode);
		}

		return oNode;
	};

	/**
	 * Returns the node state for the given group id expanded, collapsed, selected, deselected
	 *
	 * @param {object} sGroupID ID of a group node.
	 * @returns {object} node state
	 * @private
	 * @since 1.0.0
	 */
	FHIRTreeBinding.prototype._getNodeState = function(sGroupID) {
		var oExpanded = this._mTreeState.expanded[sGroupID];
		var oCollapsed = this._mTreeState.collapsed[sGroupID];
		var oSelected = this._mTreeState.selected[sGroupID];
		var oDeselected = this._mTreeState.deselected[sGroupID];

		// return one or the other
		// may be undefined if no sections loaded yet
		return oExpanded || oCollapsed || oSelected || oDeselected;
	};

	/**
	 * Calculates a unique group ID for a given node
	 *
	 * @param {object} oNode Node of which the group ID shall be calculated
	 * @returns {string} Group ID for oNode
	 * @override
	 * @private
	 * @since 1.0.0
	 */
	FHIRTreeBinding.prototype._calculateGroupID = function(oNode) {
		var sBindingPath = this.getPath();
		var sGroupId;
		if (oNode.context) {
			var sContextPath = oNode.context.getPath();
			// only split the contextpath along the binding path, if it is not the top-level ("/"),
			// otherwise the "_" replace regex, will replace wrongly substitute the context-path
			if (sBindingPath !== "/") {
				// match the context-path in case the "arrayNames" property of the ClientTreeBindings is identical to the binding path
				var aMatch = sContextPath.match(sBindingPath + "(.*)");
				if (aMatch && aMatch[1]) {
					sGroupId = aMatch[1];
				} else {
					Log.warning("CTBA: BindingPath/ContextPath matching problem!");
				}
			}
			if (!sGroupId) {
				sGroupId = sContextPath;
			}

			// slashes are used to separate levels. As in the data model not every path-part represents a level,
			// the remaining slashes must be replaced by some other character. "_" is used
			if (sGroupId.startsWith("/")) {
				sGroupId = sGroupId.substring(1, sGroupId.length);
			}

			var sParentGroupId;
			if (!oNode.parent) {
				// If there is no parent object we expect that:
				// 1. the parent group id is unknown and
				// 2. the parent context is known (added in ClientTreeBinding._applyFilterRecursive)
				//
				// We use the parent context to recursively calculate the parent group id
				// In case the parent context is empty, we expect this node to be a child of the root node (which has a context of null)
				sParentGroupId = this._calculateGroupID({
					context : oNode.context._parentContext || null
				});
			} else {
				// "Normal" case: We know the parent group id
				sParentGroupId = oNode.parent.groupID;
			}
			sGroupId = sParentGroupId + sGroupId.replace(/\//g, "_") + "/";

		} else if (oNode.context === null) {
			// only the root node should have null as context
			sGroupId = "/";
		}

		return sGroupId;
	};

	/**
	 * Creates a node state depending on the given parameters
	 *
	 * @param {object} mParameters the properties for the node state
	 * @param {string} [mParameters.groupID] The group where the node belongs to
	 * @param {boolean} [mParameters.expanded] if the node is initially expanded
	 * @param {object[]} [mParameters.sections] The section length how many childs the node has and there indices
	 * @param {number} [mParameters.sections[0].startIndex mParameters.sections[0].length] The first index everytime 0. The last index of the child node
	 * @param {boolean} [mParameters.sum] true for the root node
	 * @returns {object} node state
	 * @override
	 * @private
	 * @since 1.0.0
	 */
	FHIRTreeBinding.prototype._createNodeState = function(mParameters) {
		if (!mParameters.groupID) {
			Log.fatal("To create a node state a group ID is mandatory!");
			return undefined;
		}

		// check if the tree has an initial expansion state for the given groupID
		var bInitiallyExpanded;
		var bInitiallyCollapsed;
		if (this._oInitialTreeState) {
			bInitiallyExpanded = this._oInitialTreeState._isExpanded(mParameters.groupID);
			bInitiallyCollapsed = this._oInitialTreeState._isCollapsed(mParameters.groupID);

			this._oInitialTreeState._remove(mParameters.groupID);
		}

		// check the expansion state which should be set
		// the given values have precedence over the initially set values, false is the fallback
		var bIsExpanded = mParameters.expanded || bInitiallyExpanded || false;
		var bIsSelected = mParameters.selected || false;

		var oNodeState = {
			groupID : mParameters.groupID,
			expanded : bIsExpanded,
			// a fresh node state has to have a single page with the current pagesize
			sections : mParameters.sections || [
				{
					startIndex : 0,
					length : this._iPageSize
				}
			],
			sum : mParameters.sum || false,
			selected : bIsSelected
		};

		// track initally modified nodes in the global treeState
		if (bInitiallyExpanded || bInitiallyCollapsed) {
			this._updateTreeState({
				groupID : mParameters.groupID,
				fallbackNodeState : oNodeState,
				expanded : bInitiallyExpanded,
				collapsed : bInitiallyCollapsed
			});
		}

		return oNodeState;
	};

	/**
	 * @typedef {object} UpdateTreeStateParameters
	 * @prop {boolean} expanded
	 * @prop {string} groupID
	 * @prop {number} sum
	*/

	/**
	 * Updates the tree state depending on the given parameters
	 *
	 * @param {UpdateTreeStateParameters} mParameters Parameters used to update the tree state.
	 * @returns {object} node state
	 * @override
	 * @private
	 * @since 1.0.0
	 */
	FHIRTreeBinding.prototype._updateTreeState = function(mParameters) {
		mParameters = mParameters || {};
		// get the source and target list
		var oTargetStateObject = mParameters.expanded ? this._mTreeState.expanded : this._mTreeState.collapsed;
		var oSourceStateObject = mParameters.expanded ? this._mTreeState.collapsed : this._mTreeState.expanded;

		// get the current node state, or create a new one
		var oNodeStateInSource = this._getNodeState(mParameters.groupID);
		// if no node state exists -> create it
		if (!oNodeStateInSource) {
			oNodeStateInSource = mParameters.fallbackNodeState || this._createNodeState({
				groupID : mParameters.groupID,
				expanded : mParameters.expanded,
				sum : mParameters.sum
			});
		}

		// move from the source state to the target state
		delete oSourceStateObject[mParameters.groupID];

		oTargetStateObject[mParameters.groupID] = oNodeStateInSource;

		// keep track of the expanded status on the node state
		oNodeStateInSource.expanded = mParameters.expanded;

		return oNodeStateInSource;
	};

	/**
	 * Determines the size of a group from given node
	 *
	 * @param {object} oNode the node for which the group size should be determined
	 * @override
	 * @private
	 * @since 1.0.0
	 */
	FHIRTreeBinding.prototype._getGroupSize = function(oNode) {
		return this.getChildCount(oNode.context);
	};

	/**
	 * Requests and creates the child contexts of a given node
	 *
	 * @param {object} oNode creates the child contexts for the given node
	 * @private
	 * @since 1.0.0
	 */
	FHIRTreeBinding.prototype._loadChildContexts = function(oNode) {
		var oNodeState = oNode.nodeState;
		var aChildContexts;
		// iterate all loaded (known) sections
		for (var i = 0; i < oNodeState.sections.length; i++) {

			var oCurrentSection = oNodeState.sections[i];
			var _fnProcessNodeContexts = function(oData) { // eslint-disable-line no-loop-func
				var aKeys = [];
				for (var k in oData.entry) {
					var oResource = oData.entry[k].resource;
					aKeys[k] = oResource.resourceType + "/" + oResource.id;
				}
				var iStartIndexSubkeys = this.aKeys.indexOf(oNode.context.sPath.substring(1)) + 1;
				var iEndIndexSubkeys = iStartIndexSubkeys + aKeys.length;
				var aKeysTmp = this.aKeys.slice(iStartIndexSubkeys, iEndIndexSubkeys);
				if (!deepEqual(aKeys, aKeysTmp)) {
					FHIRUtils.insertArrayIntoArray(this.aKeys, aKeys, iStartIndexSubkeys);
					this._buildContexts();
					this.iExpandedNodesLength += aKeys.length;
					this.iTotalLength += aKeys.length;
					aChildContexts = this.aContexts.slice(iStartIndexSubkeys, iEndIndexSubkeys);
					this._iterateChildContexts(aChildContexts, oCurrentSection, oNode);
				}
			}.bind(this);
			if (oNodeState.expanded) {
				// try to load the contexts for this sections (may be [])
				if (oNode.isArtificial) {
					if (oNode.children.length === 0) {
						aChildContexts = this.aContexts;
					} else if (oNode.children.length !== oCurrentSection.length) {
						aChildContexts = this.aContexts.slice(this.aContexts.length - (oCurrentSection.length - oCurrentSection.startIndex), this.aContexts.length);
					}
					if (aChildContexts !== undefined){
						this._iterateChildContexts(aChildContexts, oCurrentSection, oNode);
					}
				} else if (!oNode.isLeaf && oNode.children.length === 0) {
					this.getNodeContexts(oNode.context, _fnProcessNodeContexts);
				} else if (!oNode.isLeaf && oNode.children.length > 0) {
					for (var j = 0; j < oNode.children.length; j++) {
						this._loadChildContexts(oNode.children[j]);
					}
					var aKeys = [];
					each(oNode.children, function(k, oChildNode) { // eslint-disable-line no-loop-func
						aKeys[k] = oChildNode.context.sPath.substring(1);
					});
					if (!FHIRUtils.isSubset(aKeys, this.aKeys)) {
						var iStartIndexSubkeys = this.aKeys.indexOf(oNode.context.sPath.substring(1)) + 1;
						FHIRUtils.insertArrayIntoArray(this.aKeys, aKeys, iStartIndexSubkeys);
						this._buildContexts();
						this.iExpandedNodesLength += aKeys.length;
						this.iTotalLength += aKeys.length;
					}
				}
			} else {
				this._removeFromKeysAndContexts(oNode);
			}
		}
	};

	/**
	 * Removes the children and their childrens from the tree by the given node
	 * @param {object} oNode - to remove its children and children's children
	 * @private
	 * @since 1.0.0
	 */
	FHIRTreeBinding.prototype._removeFromKeysAndContexts = function(oNode) {
		var aContexts = [];
		this._flatTree(oNode, aContexts, oNode, function(oCurrentNode) {
			return oCurrentNode.context;
		});
		var aKeys = [];
		each(aContexts, function(iIndex, oContext) {
			var sKey = oContext.sPath.substring(1);
			if (this.aKeys.indexOf(sKey) > -1){
				aKeys.push(sKey);
			}
		}.bind(this));
		this.aKeys = FHIRUtils.removeArrayFromArray(this.aKeys, aKeys);
		this._buildContexts();
		this.iExpandedNodesLength -= aKeys.length;
		this.iTotalLength -= aKeys.length;
	};

	/**
	 * Removes the given node from tree
	 *
	 * @param {object} oNode the node from where the tree should be flatten
	 * @param {object} aArray where all nodes are flatten after the method
	 * @param {object} oRootNode that should be excluded of the flatten tree, must be same object as oNode
	 * @param {function} fnPreProcessResult preprocess the objects which are stored in aArray
	 * @param {function} fnDoProcessOfChildren inlucde e.g. only expanded nodes in the aArray
	 * @private
	 * @since 1.0.0
	 */
	FHIRTreeBinding.prototype._flatTree = function(oNode, aArray, oRootNode, fnPreProcessResult, fnDoProcessOfChildren) {
		if (oNode !== oRootNode) {
			aArray.push(fnPreProcessResult ? fnPreProcessResult(oNode) : oNode);
		}
		if (oNode && oNode.children && (!fnDoProcessOfChildren || fnDoProcessOfChildren(oNode))) {
			for (var i = 0; i < oNode.children.length; i++) {
				this._flatTree(oNode.children[i], aArray, oRootNode, fnPreProcessResult, fnDoProcessOfChildren);
			}
		}
	};

	/**
	 * Iterates over all child contexts and handles their child aggregations
	 *
	 * @param {object} aChildContexts from oNode which are iterated
	 * @param {object} oCurrentSection  current section
	 * @param {object} oNode for processing its children
	 * @private
	 * @since 1.0.0
	 */
	FHIRTreeBinding.prototype._iterateChildContexts = function(aChildContexts, oCurrentSection, oNode) {
		// for each child context we create a new node
		each(aChildContexts, function(j, oChildContext) {
			if (oChildContext) {
				var oRequestHandle = this._loadNumberOfChildren(oChildContext, function(oData) {
					this._processChildren(oNode, oChildContext, j + oCurrentSection.startIndex, oData);
				}.bind(this));
				oRequestHandle.getRequest().always(function(oGivenRequestHandle) {
					delete this.mRequestHandle[oGivenRequestHandle.getId()];
					if (FHIRUtils.isEmptyObject(this.mRequestHandle)) {
						this.bPendingRequest = false;
						this._mTreeStateOld = FHIRUtils.deepClone(this._mTreeState);
						this._fireChange({reason : ChangeReason.Change});
					}
				}.bind(this, oRequestHandle));
				this.mRequestHandle[oRequestHandle.getId()] = oRequestHandle;
			}
		}.bind(this));
	};

	/**
	 * @typedef {object} ProcessChildrenData
	 * @prop {boolean | undefined} expanded
	 * @prop {number} total
	 */

	/**
	 * Handles child aggregations if it is leaf or not, which position, plus updating its state (expanded, collapsed ..)
	 *
	 * @param {object} oNode the parent of the childcontext
	 * @param {object} oChildContext the current processed child
	 * @param {object} iChildIndex that it can be resolved which children it is in oNode
	 * @param {ProcessChildrenData} oData Data to process.
	 * @returns {object} Built child node from oNode
	 * @private
	 * @since 1.0.0
	 */
	FHIRTreeBinding.prototype._processChildren = function(oNode, oChildContext, iChildIndex, oData) {
		var oChildNode = oNode.children[iChildIndex];

		// the updated node data after this tree building cycle
		var oUpdatedNodeData = {
			context : oChildContext,
			parent : oNode,
			level : oNode.level + 1,
			positionInParent : iChildIndex,
			autoExpand : Math.max(oNode.autoExpand - 1, -1)
		};

		// if we already have a child node reuse it, otherwise create a new one
		// Using an object reference allows us to automatically update our "snapshot" of the tree, we retrieve in getContexts
		if (oChildNode) {
			oChildNode.context = oUpdatedNodeData.context;
			oChildNode.parent = oUpdatedNodeData.parent;
			oChildNode.level = oUpdatedNodeData.level;
			oChildNode.positionInParent = oUpdatedNodeData.positionInParent;
			oChildNode.magnitude = 0;
			oChildNode.numberOfTotals = 0;
			oChildNode.autoExpand = oUpdatedNodeData.autoExpand;
			// calculate the group id for the given context
			// if we reach this point, the binding returned a context from which we can calculate the group id
			var sGroupIDForChild = this._calculateGroupID(oChildNode);
			oChildNode.groupID = sGroupIDForChild;
		} else {
			// create a node one level deeper (missing a group ID and a context)
			oChildNode = this._createNode(oUpdatedNodeData);
		}

		// retrieve the node state OR create one if necessary
		oChildNode.nodeState = this._getNodeState(oChildNode.groupID);
		if (!oChildNode.nodeState) {
			oChildNode.nodeState = this._createNodeState({
				groupID : oChildNode.groupID,
				expanded : oData.expanded || false,
				sections : [
					{
						startIndex : 0,
						length : oData.total
					}
				]
			// a new node state is never expanded (EXCEPT during auto expand!)
			});
		}

		oChildNode.nodeState.parentGroupID = oNode.groupID;
		oNode.children[iChildIndex] = oChildNode;

		// if the table is grouped: a leaf is a node 1 level deeper than the number of grouped columns
		// otherwise if the table is (fully) ungrouped every node is a leaf
		oChildNode.isLeaf = !this.nodeHasChildren(oChildNode, oData.total);

		if (oChildNode.isLeaf) {
			oNode.numberOfLeafs += 1;
		}

		// if the parent node is in selectAllMode, select this child node
		if (oChildNode.parent.nodeState.selectAllMode && !this._mTreeState.deselected[oChildNode.groupID]) {
			this.setNodeSelection(oChildNode.nodeState, true);
		}

		// if the child node was previously expanded, it has to be expanded again after we rebuilt our tree
		// --> recursion
		// but only if we have at least 1 group (otherwise we have a flat list and not a tree)
		if ((oChildNode.autoExpand > 0 || oChildNode.nodeState.expanded) && !this.aFilters) {

			if (!this._mTreeState.collapsed[oChildNode.groupID] && !oChildNode.isLeaf) {
				this._updateTreeState({
					groupID : oChildNode.nodeState.groupID,
					fallbackNodeState : oChildNode.nodeState,
					expanded : true
				});
				this._loadChildContexts(oChildNode);
			}
			// sum up the magnitude/sumRows when moving up in the recursion
			oNode.magnitude += Math.max(oChildNode.magnitude || 0, 0);
			oNode.numberOfLeafs += oChildNode.numberOfLeafs;
		}
		return oChildNode;
	};

	/**
	 * Returns true if the binding is grouped, default is true.
	 * @returns {boolean} True if the binding is grouped, otherwise false.
	 * @protected
	 * @since 1.0.0
	 */
	FHIRTreeBinding.prototype.isGrouped = function() {
		return true;
	};

	/**
	 * Creates the initial tree state
	 *
	 * @param {boolean} bReset if the tree should be rested
	 * @private
	 * @since 1.0.0
	 */
	FHIRTreeBinding.prototype._createTreeState = function(bReset) {
		if (!this._mTreeState || bReset) {
			// general tree status information, the nodes are referenced by their groupID
			this._mTreeState = {
				expanded : {}, // a map of all expanded nodes
				collapsed : {}, // a map of all collapsed nodes
				selected : {}, // a map of all selected nodes
				deselected : {}
			// a map of all deselected nodes (due to user interaction)
			};
		}
	};

	/**
	 * Returns the max depth of expanded levels
	 *
	 * @returns {number} Number of expanded levels.
	 * @protected
	 * @since 1.0.0
	 */
	FHIRTreeBinding.prototype.getNumberOfExpandedLevels = function() {
		return this.iNumberOfExpandedLevels;
	};

	/**
	 * Sets the max depth of expanded levels
	 * @param {number} iNumberOfExpandedLevels Number of expanded levels.
	 * @protected
	 * @since 1.0.0
	 */
	FHIRTreeBinding.prototype.setNumberOfExpandedLevels = function(iNumberOfExpandedLevels) {
		this.iNumberOfExpandedLevels = parseInt(iNumberOfExpandedLevels, 10);
	};

	/**
	 * Toggles the tree node sitting at the given index.
	 *
	 * @param {number} iIndex the absolute row index
	 * @protected
	 * @since 1.0.0
	 */
	FHIRTreeBinding.prototype.toggleIndex = function(iIndex) {
		var oNode = this.findNode(iIndex);

		if (!oNode) {
			Log.fatal("There is no node at index " + iIndex + ".");
			return;
		}

		if (oNode.nodeState.expanded) {
			this.collapse(iIndex);
		} else {
			this.expand(iIndex);
		}
	};

	/**
	 * Collapses the given node, identified via an absolute row index.
	 *
	 * @param {boolean} bSuppressChange if set to true, no change event will be fired
	 * @param {object} oNode last interacted node
	 * @private
	 * @since 1.0.0
	 */
	FHIRTreeBinding.prototype._handleLastNodeInteraction = function(bSuppressChange, oNode){
		if (this.vNodeLastInteraction === this._oRootNode){
			this.vNodeLastInteraction = [];
		}
		if (bSuppressChange || this.vNodeLastInteraction.length > 0){
			this.vNodeLastInteraction.push(oNode);
		} else {
			this.vNodeLastInteraction = oNode;
		}
	};

	/**
	 * Collapses the given node, identified via an absolute row index.
	 *
	 * @param {number} vParam the row index of the tree node
	 * @param {boolean} bSuppressChange if set to true, no change event will be fired
	 * @protected
	 * @since 1.0.0
	 */
	FHIRTreeBinding.prototype.collapse = function(vParam, bSuppressChange) {
		this._mTreeStateOld = FHIRUtils.deepClone(this._mTreeState);
		var that = this;
		var oNode = this.findNode(vParam);
		this._handleLastNodeInteraction(bSuppressChange, oNode);
		if (!oNode) {
			Log.fatal("No node found for index " + vParam);
			return;
		}
		var oNodeStateForCollapsingNode = oNode.nodeState;

		this._updateTreeState({
			groupID : oNodeStateForCollapsingNode.groupID,
			fallbackNodeState : oNodeStateForCollapsingNode,
			expanded : false
		});

		// remove selectAllMode if necessary
		oNodeStateForCollapsingNode.selectAllMode = false;

		if (this.bCollapseRecursive) {
			var sGroupIDforCollapsingNode = oNodeStateForCollapsingNode.groupID;
			var sGroupID;

			// Collapse all subsequent child nodes, this is determined by a common groupID prefix, e.g.: "/A100-50/" is the parent of "/A100-50/Finance/"
			// All expanded nodes which start with 'sGroupIDforCollapsingNode', are basically children of it and also need to be collapsed
			for (sGroupID in this._mTreeState.expanded) {
				if (sGroupID.startsWith(sGroupIDforCollapsingNode)) {
					that._updateTreeState({
						groupID : sGroupID,
						expanded : false
					});
				}
			}

			var aDeselectedNodeIds = [];

			// always remove selections from child nodes of the collapsed node

			for (sGroupID in this._mTreeState.selected) {
				var oNodeState = this._mTreeState.selected[sGroupID];
				if (sGroupID.startsWith(sGroupIDforCollapsingNode) && sGroupID !== sGroupIDforCollapsingNode) {
					// removes the selectAllMode from child nodes
					oNodeState.selectAllMode = false;
					that.setNodeSelection(oNodeState, false);
					aDeselectedNodeIds.push(sGroupID);
				}
			}

			if (aDeselectedNodeIds.length) {
				var selectionChangeParams = {
					rowIndices : []
				};
				// Collect the changed indices
				var iNodeCounter = -1;
				this._map(this._oRootNode, function(oCurrentNode) {
					if (!oCurrentNode || !oCurrentNode.isArtificial) {
						iNodeCounter++;
					}

					if (oCurrentNode && aDeselectedNodeIds.indexOf(oCurrentNode.groupID) !== -1) {
						if (oCurrentNode.groupID === this._sLeadSelectionGroupID) {
							// Lead selection got deselected
							selectionChangeParams.oldIndex = iNodeCounter;
							selectionChangeParams.leadIndex = -1;
						}
						selectionChangeParams.rowIndices.push(iNodeCounter);
					}
				});

				this._publishSelectionChanges(selectionChangeParams);
			}
		}

		if (!bSuppressChange) {
			this._fireChange({
				reason : ChangeReason.Collapse
			});
		}
	};

	/**
	 * Expand the tree node sitting at the given index.
	 *
	 * @param {number} iIndex the absolute row index
	 * @param {boolean} bSuppressChange if set to true, no change event will be fired
	 * @protected
	 * @since 1.0.0
	 */
	FHIRTreeBinding.prototype.expand = function(iIndex, bSuppressChange) {
		this._mTreeStateOld = FHIRUtils.deepClone(this._mTreeState);
		var oNode = this.findNode(iIndex);
		if (!oNode) {
			Log.fatal("No node found for index " + iIndex);
			return;
		}
		this._handleLastNodeInteraction(bSuppressChange, oNode);
		this._updateTreeState({
			groupID : oNode.nodeState.groupID,
			fallbackNodeState : oNode.nodeState,
			expanded : true
		});

		if (!bSuppressChange) {
			this._fireChange({
				reason : ChangeReason.Expand
			});
		}
	};

	/**
	 * Retrieves the requested part from the tree and returns node objects.
	 *
	 * @param {number} iStartIndex @see sap.fhir.model.r4.FHIRTreeBinding#getContexts
	 * @param {number} iLength @see sap.fhir.model.r4.FHIRTreeBinding#getContexts
	 * @param {number} iThreshold @see sap.fhir.model.r4.FHIRTreeBinding#getContexts
	 * @returns {object} Tree Node
	 * @protected
	 * @since 1.0.0
	 */
	FHIRTreeBinding.prototype.getNodes = function(iStartIndex, iLength, iThreshold) {
		return this.getContexts(iStartIndex, iLength, iThreshold, true);
	};

	/**
	 * Returns true or false, depending on the child count of the given node.
	 *
	 * @param {object} oNode Node instance to check whether it has children
	 * @param {number} iNumberOfChildren The number of children the node will have
	 * @returns {boolean} True if the node has children
	 * @protected
	 * @since 1.0.0
	 */
	FHIRTreeBinding.prototype.nodeHasChildren = function(oNode, iNumberOfChildren) {
		// check if the node has children
		if (!oNode) {
			return false;
		} else if (oNode.isArtificial || iNumberOfChildren > 0) {
			return true;
		} else if (oNode.isLeaf === false){
			return true;
		} else {
			// call the children
			return oNode.children.length > 0;
		}
	};

	/**
	 * Determines how many children the context has
	 *
	 * @param {object} oChildContext the info for the children
	 * @param {function} fCallback The callback to process the response, fill the aKeys
	 * @returns {sap.fhir.model.r4.lib.RequestHandle} A request handle.
	 * @private
	 * @since 1.0.0
	 */
	FHIRTreeBinding.prototype._loadNumberOfChildren = function(oChildContext, fCallback) {
		var oResource = this.oModel.getProperty(undefined, oChildContext);
		var mParameters = {
			urlParameters : {_count : 1}
		};
		mParameters.urlParameters[this.sRootSearch] = oResource[this.sNodeProperty];
		return this._submitRequest(this.sPath, mParameters, fCallback);
	};

	/**
	 * Return node contexts for the tree
	 *
	 * @param {object} oContext to use for retrieving the node contexts
	 * @param {function} fCallback The callback which is executed after the mapping of the data
	 * @protected
	 * @since 1.0.0
	 */
	FHIRTreeBinding.prototype.getNodeContexts = function(oContext, fCallback) {
		var oResource = this.oModel.getProperty(undefined, oContext);
		var mParameters = { urlParameters : {}};
		mParameters.urlParameters[this.sRootSearch] = oResource[this.sNodeProperty];
		this._submitRequest(this.sPath, mParameters, undefined, fCallback);
	};

	/**
	 * Retrieves the expanded state of the row sitting at the given index.
	 * @param {number} iIndex the index for which the expansion state should be retrieved
	 * @returns {boolean} if the given index is expanded
	 * @protected
	 * @since 1.0.0
	 */
	FHIRTreeBinding.prototype.isExpanded = function (iIndex) {
		var oNode = this.findNode(iIndex);
		return oNode && oNode.nodeState ? oNode.nodeState.expanded : false;
	};

	/**
	 * Calls a function on every child node in the sub tree with root "oNode".
	 *
	 * @param {object} oNode the starting node for the function mapping
	 * @param {function} fnMapFunction the function which should be mapped for each node in the sub-tree
	 * @private
	 * @since 1.0.0
	 */
	FHIRTreeBinding.prototype._map = function (oNode, fnMapFunction) {

		fnMapFunction.call(this, oNode);

		//if the node is not defined: there is a missing section in our tree
		if (!oNode) {
			return;
		}

		for (var i = 0; i < oNode.children.length; i++) {
			var oChildNode = oNode.children[i];
			this._map(oChildNode, fnMapFunction);
		}

		if (this._afterMapHook) {
			this._afterMapHook(oNode, fnMapFunction);
		}
	};

	//*************************************************
	//*               Selection-Handling              *
	//************************************************/

	/**
	 * Sets the selection state of the given node.
	 * @param {object} oNodeState the node state for which the selection should be changed
	 * @param {boolean} bIsSelected the selection state for the given node
	 * @protected
	 * @since 1.0.0
	 */
	FHIRTreeBinding.prototype.setNodeSelection = function (oNodeState, bIsSelected) {

		if (!oNodeState.groupID) {
			Log.fatal("NodeState must have a group ID!");
			return;
		}

		oNodeState.selected = bIsSelected;

		// toggles the selection state based on bIsSelected
		if (bIsSelected) {
			this._mTreeState.selected[oNodeState.groupID] = oNodeState;
			delete this._mTreeState.deselected[oNodeState.groupID];
		} else {
			delete this._mTreeState.selected[oNodeState.groupID];
			this._mTreeState.deselected[oNodeState.groupID] = oNodeState;
		}
	};

	/**
	 * Returns the selection state for the node at the given index.
	 * @param {number} iRowIndex the row index to check for selection state
	 * @returns {boolean} If row is selected
	 * @protected
	 * @since 1.0.0
	 */
	FHIRTreeBinding.prototype.isIndexSelected = function (iRowIndex) {
		var oNode = this.getNodeByIndex(iRowIndex);
		return oNode && oNode.nodeState ? oNode.nodeState.selected : false;
	};

	/**
	 * Returns if the node at the given index is selectable.
	 * In the AnalyticalTable only nodes with isLeaf = true are selectable.
	 * @param {number} iRowIndex the row index which should be checked for "selectability"
	 * @returns {boolean} If row is selectable
	 * @protected
	 * @since 1.0.0
	 */
	FHIRTreeBinding.prototype.isIndexSelectable = function (iRowIndex) {
		var oNode = this.getNodeByIndex(iRowIndex);
		return this._isNodeSelectable(oNode);
	};

	/**
	 * Checks if the given node can be selected. Always true for TreeTable controls, except the node is not defined.
	 * @param {object} oNode The node which should be checked
	 * @returns {boolean} If the node is selectable
	 * @private
	 * @since 1.0.0
	 */
	FHIRTreeBinding.prototype._isNodeSelectable = function (oNode) {
		return !!oNode && !oNode.isArtificial;
	};

	/**
	 * Marks a single TreeTable node sitting on iRowIndex as selected.
	 * Also sets the lead selection index to this node.
	 * @param {number} iRowIndex the absolute row index which should be selected
	 * @protected
	 * @since 1.0.0
	 */
	FHIRTreeBinding.prototype.setSelectedIndex = function (iRowIndex) {
		var oNode = this.findNode(iRowIndex);

		if (oNode && this._isNodeSelectable(oNode)) {
			// clear and fetch the changes on the selection
			var oChanges = this._clearSelection();

			// if the selected row index was already selected before -> remove it from the changed Indices from the clearSection() call
			var iChangedIndex = oChanges.rowIndices.indexOf(iRowIndex);
			if (iChangedIndex >= 0) {
				oChanges.rowIndices.splice(iChangedIndex, 1);
			} else {
				// the newly selected index is missing and also has to be propagated via the event params
				oChanges.rowIndices.push(iRowIndex);
			}

			//set the new lead index
			oChanges.leadGroupID = oNode.groupID;
			oChanges.leadIndex = iRowIndex;

			this.setNodeSelection(oNode.nodeState, true);

			this._publishSelectionChanges(oChanges);
		} else {
			Log.warning("The selection was ignored. Please make sure to only select rows, for which data has been fetched to the client. For AnalyticalTables, some rows might not be selectable at all.");
		}
	};


	/**
	 * Returns an array with all selected row indices.
	 * Only absolute row indices for nodes known to the client will can be retrieved this way
	 * @returns {number[]} an array with all selected indices
	 * @protected
	 * @since 1.0.0
	 */
	FHIRTreeBinding.prototype.getSelectedIndices = function () {
		var aResultIndices = [];
		var that = this;

		//if we have no nodes selected, the selection indices are empty
		if (FHIRUtils.isEmptyObject(this._mTreeState.selected)) {
			return aResultIndices;
		}

		// maximum number of possibly selected nodes
		var iNumberOfNodesToSelect = Object.keys(this._mTreeState.selected).length;

		// collect the indices of all selected nodes
		var iNodeCounter = -1;
		var fnMatchFunction = function (oNode) {
			if (!oNode || !oNode.isArtificial) {
				iNodeCounter++;
			}

			if (oNode) {
				if (oNode.nodeState && oNode.nodeState.selected && !oNode.isArtificial) {
					aResultIndices.push(iNodeCounter);
					// cache the selected node for subsequent findNode/getContextByIndex calls
					that._aRowIndexMap[iNodeCounter] = oNode;
					return true;
				}
			}
			return undefined;
		};

		this._match(this._oRootNode, [], iNumberOfNodesToSelect, fnMatchFunction);

		return aResultIndices;
	};

	/**
	 * Returns the number of selected nodes (including not-yet loaded)
	 * @returns {number} How many nodes are selected
	 * @protected
	 * @since 1.0.0
	 */
	FHIRTreeBinding.prototype.getSelectedNodesCount = function () {
		var iSelectedNodes;

		if (this._oRootNode && this._oRootNode.nodeState.selectAllMode) {
			var sGroupId, iVisibleDeselectedNodeCount, oParent, oGroupNodeState;

			var oContext, aVisibleGroupIds = [];
			if (this.filterInfo && this.aAllFilters) {
				// If we are filtering, we need to map the filtered (visible) contexts to group IDs.
				// With that we can check whether a node state is actually a visible node
				for (var i = this.filterInfo.aFilteredContexts.length - 1; i >= 0; i--) {
					oContext = this.filterInfo.aFilteredContexts[i];
					aVisibleGroupIds.push(this._calculateGroupID({
						context: oContext
					}));
				}
			}

			iVisibleDeselectedNodeCount = 0;
			// If we implicitly deselect all nodes under a group node,
			//	we need to count them as "visible deselected nodes"
			for (sGroupId in this._mTreeState.expanded) {
				if (!this.aAllFilters || aVisibleGroupIds.indexOf(sGroupId) !== -1) { // Not filtering or part of the visible nodes if filtering
					oGroupNodeState = this._mTreeState.expanded[sGroupId];
					if (!oGroupNodeState.selectAllMode && oGroupNodeState.leafCount !== undefined) {
						iVisibleDeselectedNodeCount += oGroupNodeState.leafCount;
					}
				}
			}

			// Except those who got explicitly selected after the parent got collapsed
			//	and expanded again (and while the root is still in select-all mode)
			for (sGroupId in this._mTreeState.selected) {
				if (!this.aAllFilters || aVisibleGroupIds.indexOf(sGroupId) !== -1) { // Not filtering or part of the visible nodes if filtering
					oGroupNodeState = this._mTreeState.selected[sGroupId];
					oParent = this._mTreeState.expanded[oGroupNodeState.parentGroupID];
					if (oParent && !oParent.selectAllMode) {
						iVisibleDeselectedNodeCount--;
					}
				}
			}

			// Add those which are explicitly deselected and whose parents *are* in selectAllMode (not covered by the above)
			for (sGroupId in this._mTreeState.deselected) {
				if (!this.aAllFilters || aVisibleGroupIds.indexOf(sGroupId) !== -1) { // Not filtering or part of the visible nodes if filtering
					oGroupNodeState = this._mTreeState.deselected[sGroupId];
					oParent = this._mTreeState.expanded[oGroupNodeState.parentGroupID];
					// If parent is expanded check if its in select all mode
					if (oParent && oParent.selectAllMode) {
						iVisibleDeselectedNodeCount++;
					}
				}
			}

			iSelectedNodes = this._getSelectableNodesCount(this._oRootNode) - iVisibleDeselectedNodeCount;
		} else {
			iSelectedNodes = Object.keys(this._mTreeState.selected).length;
		}
		return iSelectedNodes;
	};

	/**
	 * Returns the number of currently selectable nodes (with respect to the current expand/collapse state).
	 * @param {object} oNode the node with it selectable subnodes information
	 * @returns {number} Number of currently selectable nodes
	 * @private
	 * @since 1.0.0
	 */
	FHIRTreeBinding.prototype._getSelectableNodesCount = function (oNode) {
		if (oNode) {
			return oNode.magnitude;
		} else {
			return 0;
		}
	};

	/**
	 * Returns an array containing all selected contexts, ordered by their appearance in the tree.
	 * @returns {sap.fhir.model.r4.Context[]} an array containing the binding contexts for all selected nodes
	 * @protected
	 * @since 1.0.0
	 */
	FHIRTreeBinding.prototype.getSelectedContexts = function () {
		var aResultContexts = [];
		var that = this;

		//if we have no nodes selected, the selection indices are empty
		if (FHIRUtils.isEmptyObject(this._mTreeState.selected)) {
			return aResultContexts;
		}

		// maximum number of possibly selected nodes
		var iNumberOfNodesToSelect = Object.keys(this._mTreeState.selected).length;

		// collect the indices & contexts of all selected nodes
		var iNodeCounter = -1;
		var fnMatchFunction = function (oNode) {
			if (!oNode || !oNode.isArtificial) {
				iNodeCounter++;
			}

			if (oNode) {
				if (oNode.nodeState && oNode.nodeState.selected && !oNode.isArtificial) {
					aResultContexts.push(oNode.context);
					// cache the selected node for subsequent findNode/getContextByIndex calls
					that._aRowIndexMap[iNodeCounter] = oNode;
					return true;
				}
			}
			return undefined;
		};

		this._match(this._oRootNode, [], iNumberOfNodesToSelect, fnMatchFunction);

		return aResultContexts;
	};

	/**
	 * Sets the selection to the range from iFromIndex to iToIndex (including boundaries).
	 * e.g. setSelectionInterval(1,3) marks the rows 1,2 and 3.
	 * All currently selected rows will be deselected in the process.
	 * A selectionChanged event is fired
	 * @param {number} iFromIndex The start of the selection interval
	 * @param {number} iToIndex The end of the selection interval
	 * @protected
	 * @since 1.0.0
	 */
	FHIRTreeBinding.prototype.setSelectionInterval = function (iFromIndex, iToIndex) {
		// clears the selection but suppresses the selection change event
		var mClearParams = this._clearSelection();
		// the addSelectionInterval function takes care of the selection change event
		var mSetParams = this._setSelectionInterval(iFromIndex, iToIndex, true);

		var mIndicesFound = {};
		var aRowIndices = [];
		var iIndex;
		// flag all cleared indices as changed
		for (var i = 0; i < mClearParams.rowIndices.length; i++) {
			iIndex = mClearParams.rowIndices[i];
			mIndicesFound[iIndex] = true;
		}

		// now merge the changed indices after clearing with the newly selected
		// duplicate indices mean, that the index was previously selected and is now still selected -> remove it from the changes
		for (var j = 0; j < mSetParams.rowIndices.length; j++) {
			iIndex = mSetParams.rowIndices[j];
			if (mIndicesFound[iIndex]) {
				delete mIndicesFound[iIndex];
			} else {
				mIndicesFound[iIndex] = true;
			}
		}
		// transform the changed index MAP into a real array of indices
		for (iIndex in mIndicesFound) {
			if (mIndicesFound[iIndex]) {
				aRowIndices.push(parseInt(iIndex, 10));
			}
		}

		//and fire the event
		this._publishSelectionChanges({
			rowIndices: aRowIndices,
			oldIndex: mClearParams.oldIndex,
			leadIndex: mSetParams.leadIndex,
			leadGroupID: mSetParams.leadGroupID
		});
	};

	/**
	 * Sets the value inside the given range to the value given with 'bSelectionValue'
	 * @private
	 * @param {number} iFromIndex the starting index of the selection range
	 * @param {number} iToIndex the end index of the selection range
	 * @param {boolean} bSelectionValue the selection state which should be applied to all indices between 'from' and 'to' index
	 * @returns {object} The selection interval parameters
	 * @private
	 * @since 1.0.0
	 */
	FHIRTreeBinding.prototype._setSelectionInterval = function (iFromIndex, iToIndex, bSelectionValue) {
		//make sure the "From" Index is always lower than the "To" Index
		var iNewFromIndex = Math.min(iFromIndex, iToIndex);
		var iNewToIndex = Math.max(iFromIndex, iToIndex);

		//find out how many nodes should be selected, this is a termination condition for the match function
		var aNewlySelectedNodes = [];
		var aChangedIndices = [];
		var iNumberOfNodesToSelect = Math.abs(iNewToIndex - iNewFromIndex) + 1; //+1 because the boundary indices are included

		// the old lead index, might be undefined -> publishSelectionChanges() will set it to -1
		var iOldLeadIndex;

		// loop through all nodes and select them if necessary
		var iNodeCounter = -1;
		var fnMatchFunction = function (oNode) {

			// do not count the artificial root node
			if (!oNode || !oNode.isArtificial) {
				iNodeCounter++;
			}

			if (oNode) {
				//if the node is inside the range -> select it
				if (iNodeCounter >= iNewFromIndex && iNodeCounter <= iNewToIndex) {

					if (this._isNodeSelectable(oNode)) {
						// fetch the node index if its selection state changes
						if (oNode.nodeState.selected !== !!bSelectionValue) {
							aChangedIndices.push(iNodeCounter);
						}

						// remember the old lead selection index if we encounter it
						// (might not happen if the lead selection is outside the newly set range)
						if (oNode.groupID === this._sLeadSelectionGroupID) {
							iOldLeadIndex = iNodeCounter;
						}

						// select/deselect node, but suppress the selection change event
						this.setNodeSelection(oNode.nodeState, !!bSelectionValue);
					}

					return true;
				}
			}
			return undefined;
		};

		this._match(this._oRootNode, aNewlySelectedNodes, iNumberOfNodesToSelect, fnMatchFunction);

		var mParams = {
			rowIndices: aChangedIndices,
			oldIndex: iOldLeadIndex,
			//if we found a lead index during tree traversal and we deselected it -> the new lead selection index is -1
			leadIndex: iOldLeadIndex && !bSelectionValue ? -1 : undefined
		};

		// set new lead selection node if necessary
		if (aNewlySelectedNodes.length > 0 && bSelectionValue){
			var oLeadSelectionNode = aNewlySelectedNodes[aNewlySelectedNodes.length - 1];
			mParams.leadGroupID = oLeadSelectionNode.groupID;
			mParams.leadIndex = iNewToIndex;
		}

		return mParams;
	};

	/**
	 * Marks a range of tree nodes as selected/deselected, starting with iFromIndex going to iToIndex.
	 * The TreeNodes are referenced via their absolute row index.
	 * Please be aware, that the absolute row index only applies to the tree which is visualized by the TreeTable.
	 * Invisible nodes (collapsed child nodes) will not be regarded.
	 * @param {number} iFromIndex the starting index of the selection range
	 * @param {number} iToIndex the end index of the selection range
	 * @protected
	 * @since 1.0.0
	 */
	FHIRTreeBinding.prototype.addSelectionInterval = function (iFromIndex, iToIndex) {
		var mParams = this._setSelectionInterval(iFromIndex, iToIndex, true);
		this._publishSelectionChanges(mParams);
	};

	/**
	 * Removes the selections inside the given range (including boundaries)
	 * @param {number} iFromIndex the starting index of the selection range
	 * @param {number} iToIndex the end index of the selection range
	 * @protected
	 * @since 1.0.0
	 */
	FHIRTreeBinding.prototype.removeSelectionInterval = function (iFromIndex, iToIndex) {
		var mParams = this._setSelectionInterval(iFromIndex, iToIndex, false);
		this._publishSelectionChanges(mParams);
	};

	/**
	 * Selects all avaliable nodes
	 * @protected
	 * @since 1.0.0
	 */
	FHIRTreeBinding.prototype.selectAll = function () {

		// remove all deselected nodes
		this._mTreeState.deselected = {};

		var mParams = {
			rowIndices: [],
			oldIndex: -1,
			selectAll: true
		};

		// recursion variables
		var iNodeCounter = -1;

		this._map(this._oRootNode, function (oNode) {

			if (!oNode || !oNode.isArtificial) {
				iNodeCounter++;
			}

			if (oNode) {

				//if we find the old lead selection index -> keep it, safes some performance later on
				if (oNode.groupID === this._sLeadSelectionGroupID) {
					mParams.oldIndex = iNodeCounter;
				}

				if (this._isNodeSelectable(oNode)) {
					//if a node is NOT selected (and is not our artificial root node...)
					if (oNode.nodeState.selected !== true) {
						mParams.rowIndices.push(iNodeCounter);
					}
					this.setNodeSelection(oNode.nodeState, true);

					// keep track of the last selected node -> this will be the new lead index
					mParams.leadGroupID = oNode.groupID;
					mParams.leadIndex = iNodeCounter;
				}

				//propagate select all mode to all expanded nodes (including the root node)
				// child nodes will inherit the selection state it in the process (see _buildTree/_loadChildContexts)
				if (oNode.nodeState.expanded) {
					oNode.nodeState.selectAllMode = true;
				}
			}
		});

		this._publishSelectionChanges(mParams);
	};

	/**
	 * Removes the selection from all nodes
	 * @returns {object} The selection interval parameters
	 * @private
	 * @since 1.0.0
	 */
	FHIRTreeBinding.prototype._clearSelection = function () {
		var iNodeCounter = -1;
		var iOldLeadIndex = -1;
		var iMaxNumberOfMatches = 0;

		var aChangedIndices = [];

		// Optimisation: find out how many nodes we have to check for deselection
		for (var sGroupID in this._mTreeState.selected) {
			if (sGroupID) {
				iMaxNumberOfMatches++;
			}
		}

		// matches all selected nodes and retrieves their absolute row index
		var fnMatch = function (oNode) {

			// do not count the artifical root node
			if (!oNode || !oNode.isArtificial) {
				iNodeCounter++;
			}

			if (oNode) {
				// Always reset selectAllMode
				oNode.nodeState.selectAllMode = false;

				if (this._mTreeState.selected[oNode.groupID]) {
					// remember changed index, push it to the limit!
					if (!oNode.isArtificial) {
						aChangedIndices.push(iNodeCounter);
					}
					// deslect the node
					this.setNodeSelection(oNode.nodeState, false);

					//also remember the old lead index
					if (oNode.groupID === this._sLeadSelectionGroupID) {
						iOldLeadIndex = iNodeCounter;
					}

					return true;
				}
			}
			return undefined;
		};

		this._match(this._oRootNode, [], iMaxNumberOfMatches, fnMatch);

		// explicitly remove the selectAllMode from the root node
		if (this._oRootNode && this._oRootNode.nodeState && this._oRootNode.isArtificial) {
			this._oRootNode.nodeState.selectAllMode = false;
		}

		return {
			rowIndices: aChangedIndices,
			oldIndex: iOldLeadIndex,
			leadIndex: -1
		};
	};
	/**
	 * Removes the complete selection.
	 * @param {boolean} bSuppressSelectionChangeEvent if this is set to true, no selectionChange event will be fired
	 * @protected
	 * @since 1.0.0
	 */
	FHIRTreeBinding.prototype.clearSelection = function (bSuppressSelectionChangeEvent) {
		var oChanges = this._clearSelection();

		// check if the selection change event should be suppressed
		if (!bSuppressSelectionChangeEvent) {
			this._publishSelectionChanges(oChanges);
		}
	};

	/**
	 * Fires a "selectionChanged" event with the given parameters.
	 * Also performs a sanity check on the parameters.
	 * @param {object} mParams The selection interval parameters
	 * @private
	 * @since 1.0.0
	 */
	FHIRTreeBinding.prototype._publishSelectionChanges = function (mParams) {

		// retrieve the current (old) lead selection and add it to the changed row indices if necessary
		mParams.oldIndex = mParams.oldIndex || this.getSelectedIndex();

		//sort row indices ascending
		mParams.rowIndices.sort(function(a, b) {
			return a - b;
		});

		//set the lead selection index
		if (mParams.leadIndex >= 0 && mParams.leadGroupID) {
			//keep track of a newly set lead index
			this._sLeadSelectionGroupID = mParams.leadGroupID;
		} else if (mParams.leadIndex === -1){
			// explicitly remove the lead index
			this._sLeadSelectionGroupID = undefined;
		} else {
			//nothing changed, lead and old index are the same
			mParams.leadIndex = mParams.oldIndex;
		}

		//only fire event if the selection actually changed somehow
		if (mParams.rowIndices.length > 0 || (mParams.leadIndex !== undefined && mParams.leadIndex !== -1)) {
			this.fireSelectionChanged(mParams);
		}
	};

	/**
	 * Sets the node hierarchy to collapse recursive. When set to true, all child nodes will get collapsed as well.
	 * @param {boolean} bCollapseRecursive If the tree should be collapse recursively
	 * @protected
	 * @since 1.0.0
	 */
	FHIRTreeBinding.prototype.setCollapseRecursive = function (bCollapseRecursive) {
		this.bCollapseRecursive = !!bCollapseRecursive;
	};

	/**
	 * Gets the collapsing behavior when parent nodes are collapsed.
	 * @returns {boolean} If the current tree binding will be collapsed recursively
	 * @protected
	 * @since 1.0.0
	 */
	FHIRTreeBinding.prototype.getCollapseRecursive = function () {
		return this.bCollapseRecursive;
	};

	/**
	 * Attach event-handler <code>fnFunction</code> to the 'selectionChanged' event of this <code>sap.ui.model.SelectionModel</code>.<br/>
	 * Event is fired if the selection of tree nodes is changed in any way.
	 *
	 * @param {object}
	 *            [oData] The object, that should be passed along with the event-object when firing the event.
	 * @param {function}
	 *            fnFunction The function to call, when the event occurs. This function will be called on the
	 *            oListener-instance (if present) or in a 'static way'.
	 * @param {object}
	 *            [oListener] Object on which to call the given function.
	 * @returns {sap.ui.model.SelectionModel} <code>this</code> to allow method chaining
	 * @protected
	 * @since 1.0.0
	 */
	FHIRTreeBinding.prototype.attachSelectionChanged = function(oData, fnFunction, oListener) {
		this.attachEvent("selectionChanged", oData, fnFunction, oListener);
		return this;
	};

	/**
	 * Detach event-handler <code>fnFunction</code> from the 'selectionChanged' event of this <code>sap.ui.model.SelectionModel</code>.<br/>
	 *
	 * The passed function and listener object must match the ones previously used for event registration.
	 *
	 * @param {function}
	 *            fnFunction The function to call, when the event occurs.
	 * @param {object}
	 *            oListener Object on which the given function had to be called.
	 * @returns {sap.ui.model.SelectionModel} <code>this</code> to allow method chaining
	 * @protected
	 * @since 1.0.0
	 */
	FHIRTreeBinding.prototype.detachSelectionChanged = function(fnFunction, oListener) {
		this.detachEvent("selectionChanged", fnFunction, oListener);
		return this;
	};

	/**
	 * Fire event 'selectionChanged' to attached listeners.
	 *
	 * Expects following event parameters:
	 * <ul>
	 * <li>'leadIndex' of type <code>int</code> Lead selection index.</li>
	 * <li>'rowIndices' of type <code>int[]</code> Other selected indices (if available)</li>
	 * </ul>
	 *
	 * @param {object} mArguments the arguments to pass along with the event.
	 * @param {number} mArguments.leadIndex Lead selection index
	 * @param {number[]} [mArguments.rowIndices] Other selected indices (if available)
	 * @returns {sap.ui.model.SelectionModel} <code>this</code> to allow method chaining
	 * @protected
	 * @since 1.0.0
	 */
	FHIRTreeBinding.prototype.fireSelectionChanged = function(mArguments) {
		this.fireEvent("selectionChanged", mArguments);
		return this;
	};

	return FHIRTreeBinding;

});