text: /*!
   * ${copyright}
   */
  
  // Disable some ESLint rules. camelcase (some "_" in names to indicate indexed variables (like in math)), valid-jsdoc (not completed yet), no-warning-comments (some TODOs are left)
  // All other warnings, errors should be resolved
  /*eslint camelcase:0, valid-jsdoc:0, no-warning-comments:0 */
  
  // Provides class sap.ui.model.odata.ODataListBinding
  sap.ui.define([
  	'sap/ui/model/TreeBinding',
  	'sap/ui/model/ChangeReason',
  	'sap/ui/model/Filter',
  	'sap/ui/model/FilterOperator',
  	'sap/ui/model/FilterProcessor',
  	'sap/ui/model/FilterType',
  	'sap/ui/model/Sorter',
  	'sap/ui/model/odata/CountMode',
  	'sap/ui/model/TreeAutoExpandMode',
  	'./odata4analytics',
  	'./BatchResponseCollector',
  	'./AnalyticalVersionInfo',
  	"sap/base/util/uid",
  	"sap/base/util/deepEqual",
  	"sap/ui/thirdparty/jquery",
  	"sap/base/Log"
  ], function(
  	TreeBinding,
  	ChangeReason,
  	Filter,
  	FilterOperator,
  	FilterProcessor,
  	FilterType,
  	Sorter,
  	CountMode,
  	TreeAutoExpandMode,
  	odata4analytics,
  	BatchResponseCollector,
  	AnalyticalVersionInfo,
  	uid,
  	deepEqual,
  	jQuery,
  	Log
  ) {
  	"use strict";
  
  	var sClassName = "sap.ui.model.analytics.AnalyticalBinding",
  		oLogger = Log.getLogger(sClassName);
  
  	/**
  	 * Checks whether the select binding parameter fits to the current analytical info and returns
  	 * an array of properties that need to be added to leaf requests. If the select binding
  	 * parameter does not fit to the current analytical info a warning is logged and the select
  	 * binding parameter is ignored.
  	 * Select binding parameter does not fit to the analytical info,
  	 * <ul>
  	 * <li>if an additional dimension is contained in the select binding parameter
  	 * <li>if an associated property (e.g. text property or attribute) of an additional dimension
  	 * is contained in the select binding parameter
  	 * <li>if an additional measure is contained in the select binding parameter
  	 * <li>if an associated property (e.g. text property) of an additional measure is contained in
  	 * the select binding parameter
  	 * <li>if a dimension or a measure of the current analytical info is not contained in the select
  	 * binding parameter
  	 * </ul>
  	 *
  	 * @param {sap.ui.model.analytics.AnalyticalBinding} oBinding
  	 *   The analytical binding instance
  	 * @returns {string[]} An array of additional properties that need to be selected or an empty
  	 *   array if there are no additional select properties needed
  	 */
  	function getAdditionalSelects(oBinding) {
  		var oAnalyticalQueryRequest
  				= new odata4analytics.QueryResultRequest(oBinding.oAnalyticalQueryResult),
  			aComputedSelect,
  			sComputedSelect,
  			oDimension,
  			i,
  			j,
  			oMeasure,
  			n,
  			sPropertyName,
  			aSelect = oBinding.mParameters.select.split(","),
  			bError = trimAndCheckForDuplicates(aSelect, oBinding.sPath);
  
  		// prepare oAnalyticalQueryRequest to be able to call getURIQueryOptionValue("$select")
  		oAnalyticalQueryRequest.setAggregationLevel(oBinding.aMaxAggregationLevel);
  		oAnalyticalQueryRequest.setMeasures(oBinding.aMeasureName);
  
  		// update dimension's key, text and attributes as done in relevant _prepare... functions
  		Object.keys(oBinding.oDimensionDetailsSet).forEach(function (sDimensionKey) {
  			oDimension = oBinding.oDimensionDetailsSet[sDimensionKey];
  
  			oAnalyticalQueryRequest.includeDimensionKeyTextAttributes(sDimensionKey,
  				true, oDimension.textPropertyName !== undefined, oDimension.aAttributeName);
  		});
  
  		// update measure's raw value, formatted value and unit property as done in relevant
  		// _prepare... functions
  		Object.keys(oBinding.oMeasureDetailsSet).forEach(function (sMeasureKey) {
  			oMeasure = oBinding.oMeasureDetailsSet[sMeasureKey];
  
  			oAnalyticalQueryRequest.includeMeasureRawFormattedValueUnit(sMeasureKey,
  				oMeasure.rawValuePropertyName !== undefined,
  				oMeasure.formattedValuePropertyName !== undefined,
  				oMeasure.unitPropertyName !== undefined);
  		});
  
  		// at least all selected properties, computed by the binding, are contained in select
  		// binding parameter
  		sComputedSelect = oAnalyticalQueryRequest.getURIQueryOptionValue("$select");
  		if (sComputedSelect) {
  			aComputedSelect = sComputedSelect.split(",");
  			for (i = 0, n = aComputedSelect.length; i < n; i++) {
  				sPropertyName = aComputedSelect[i];
  				j = aSelect.indexOf(sPropertyName);
  				if (j < 0) {
  					oLogger.warning("Ignored the 'select' binding parameter, because"
  							+ " it does not contain the property '" + sPropertyName + "'",
  						oBinding.sPath);
  					bError = true;
  				} else {
  					aSelect.splice(j, 1);
  				}
  			}
  		}
  
  		// check additionally selected properties, no new dimensions and new measures or
  		// associated properties for new dimensions or measures are allowed
  		for (i = 0, n = aSelect.length; i < n; i++) {
  			sPropertyName = aSelect[i];
  
  			oDimension = oBinding.oAnalyticalQueryResult.findDimensionByPropertyName(sPropertyName);
  			if (oDimension && oBinding.oDimensionDetailsSet[oDimension.getName()] === undefined) {
  				logUnsupportedPropertyInSelect(oBinding.sPath, sPropertyName, oDimension);
  				bError = true;
  			}
  
  			oMeasure = oBinding.oAnalyticalQueryResult.findMeasureByPropertyName(sPropertyName);
  			if (oMeasure && oBinding.oMeasureDetailsSet[oMeasure.getName()] === undefined) {
  				logUnsupportedPropertyInSelect(oBinding.sPath, sPropertyName, oMeasure);
  				bError = true;
  			}
  		}
  		return bError ? [] : aSelect;
  	}
  
  	/**
  	 * Logs a warning that the given select property is not supported. Either it is a dimension or
  	 * a measure or it is associated with a dimension or a measure which is not part of the
  	 * analytical info.
  	 *
  	 * @param {string} sPath The binding path
  	 * @param {string} sSelectedProperty The name of the selected property
  	 * @param {sap.ui.model.analytics.odata4analytics.Dimension
  	 *         |sap.ui.model.analytics.odata4analytics.Measure} oDimensionOrMeasure
  	 *   The dimension or measure that causes the issue
  	 */
  	function logUnsupportedPropertyInSelect(sPath, sSelectedProperty, oDimensionOrMeasure) {
  		var sDimensionOrMeasure = oDimensionOrMeasure
  				instanceof sap.ui.model.analytics.odata4analytics.Dimension
  					? "dimension" : "measure";
  
  		if (oDimensionOrMeasure.getName() === sSelectedProperty) {
  			oLogger.warning("Ignored the 'select' binding parameter, because it contains"
  					+ " the " + sDimensionOrMeasure + " property '"
  					+ sSelectedProperty
  					+ "' which is not contained in the analytical info (see updateAnalyticalInfo)",
  				sPath);
  
  		} else {
  			oLogger.warning("Ignored the 'select' binding parameter, because the property '"
  					+ sSelectedProperty + "' is associated with the "
  					+ sDimensionOrMeasure + " property '"
  					+ oDimensionOrMeasure.getName() + "' which is not contained in the analytical"
  					+ " info (see updateAnalyticalInfo)",
  				sPath);
  		}
  	}
  
  	/**
  	 * Iterate over the given array, trim each value and check whether there are duplicate entries
  	 * in the array. If there are duplicate entries a warning is logged and the duplicate is removed
  	 * from the array.
  	 *
  	 * @param {string[]} aSelect An array of strings
  	 * @param {string} sPath The binding path
  	 * @returns {boolean} <code>true</code> if there is at least one duplicate entry in the array.
  	 */
  	function trimAndCheckForDuplicates(aSelect, sPath) {
  		var sCurrentProperty,
  			bError = false,
  			i,
  			n;
  
  		// replace all white-spaces before and after the value
  		for (i = 0, n = aSelect.length; i < n; i++) {
  			aSelect[i] = aSelect[i].trim();
  		}
  		// check for duplicate entries and remove from list
  		for (i = aSelect.length - 1; i >= 0; i--) {
  			sCurrentProperty = aSelect[i];
  			if (aSelect.indexOf(sCurrentProperty) !== i) {
  				// found duplicate
  				oLogger.warning("Ignored the 'select' binding parameter, because it"
  						+ " contains the property '" + sCurrentProperty + "' multiple times",
  					sPath);
  				aSelect.splice(i, 1);
  				bError = true;
  			}
  		}
  		return bError;
  	}
  
  	/**
  	 * @class
  	 * Tree binding implementation for OData entity sets with aggregate semantics.
  	 *
  	 * Note on the handling of different count modes:
  	 * The AnalyticalBinding always uses the OData $inlinecount system query option to determine the total
  	 * count of matching entities. It ignores the default count mode set in the ODataModel instance and the
  	 * count mode specified in the binding parameters. If the default count mode is None, a warning is added
  	 * to the log to remind the application that OData requests generated by the AnalyticalBinding will include
  	 * a $inlinecount. If a count mode has been specified in the binding parameters, an error message will be
  	 * logged if it is None, because the binding will still add the $inlinecount to OData requests. If a
  	 * binding count mode is set to Request or Both, a warning will be logged to remind the application that
  	 * the OData requests generated by the AnalyticalBinding will include a $inlinecount.
  	 *
  	 * @param {sap.ui.model.Model} oModel
  	 * @param {string}
  	 *            sPath the path pointing to the tree / array that should be bound
  	 * @param {object}
  	 *            [oContext=null] the context object for this data binding
  	 * @param {array}
  	 *            [aSorter=null] predefined sorter/s contained in an array
  	 * @param {array}
  	 *            [aFilters=null] predefined filter/s contained in an array
  	 * @param {object} [mParameters=null] a map which contains additional control parameters.
  	 * @param {sap.ui.model.TreeAutoExpandMode} [mParameters.autoExpandMode=sap.ui.model.TreeAutoExpandMode.Bundled]
  	 *            the auto expand mode; applying sorters to groups is only possible with auto expand
  	 *            mode {@link sap.ui.model.TreeAutoExpandMode.Sequential}
  	 * @param [mParameters.entitySet] if set, it explicitly specifies the entity set addressed by
  	 *            the last segment of the given binding path
  	 * @param [mParameters.useBatchRequests] if true, multiple OData requests will be wrapped into a
  	 *            single batch request, wherever possible
  	 * @param [mParameters.provideGrandTotals] if true, grand total values will be provided for all
  	 *            bound measure properties
  	 * @param [mParameters.provideTotalResultSize] if true, the total number of matching entries in
  	 *            the bound OData entity set will be provided
  	 * @param [mParameters.reloadSingleUnitMeasures] if true, the binding will check aggregated
  	 *            entries with multi-unit occurrences, if some measure properties have a unique unit
  	 *            and will trigger separate OData requests to fetch them
  	 * @param {string} [mParameters.select] a comma separated list of property names that need to be
  	 *            selected.<br/>
  	 *            If the <code>select</code> parameter is given, it has to contain all properties
  	 *            that are contained in the analytical information (see
  	 *            {@link sap.ui.model.analytics.AnalyticalBinding#updateAnalyticalInfo}) and their
  	 *            associated dimensions and measures. It must not contain additional dimensions or
  	 *            measures or associated properties for additional dimensions or measures. But it
  	 *            may contain additional properties like a text property of a dimension that is also
  	 *            selected.<br/>
  	 *            All properties of the <code>select</code> parameter are also considered in
  	 *            {@link sap.ui.model.analytics.AnalyticalBinding#getDownloadUrl}.<br/>
  	 *            The <code>select</code> parameter must not contain any duplicate entry.<br/>
  	 *            If the <code>select</code> parameter does not fit to the analytical information or
  	 *            if the <code>select</code> parameter contains duplicates, a warning is logged and
  	 *            the <code>select</code> parameter is ignored.
  	 *
  	 * @throws Will throw an error if no analytic query result object could be determined from the bound OData entity set, either from an explicitly
  	 *         given EntitySet (via optional mParameters.entitySet argument), or by default implicitly from
  	 *         the binding path (see mandatory sPath argument).
  	 *
  	 * @alias sap.ui.model.analytics.AnalyticalBinding
  	 * @extends sap.ui.model.TreeBinding
  	 * @experimental This module is only for experimental use!
  	 * @protected
  	 */
  	var AnalyticalBinding = TreeBinding.extend("sap.ui.model.analytics.AnalyticalBinding", /** @lends sap.ui.model.analytics.AnalyticalBinding.prototype */ {
  
  		constructor : function(oModel, sPath, oContext, aSorter, aFilters, mParameters) {
  			TreeBinding.call(this, oModel, sPath, oContext, aFilters, mParameters);
  
  			this.aAdditionalSelects = [];
  			// attribute members for addressing the requested entity set
  			this.sEntitySetName = (mParameters && mParameters.entitySet) ? mParameters.entitySet : undefined;
  			// attribute members for maintaining aggregated OData requests
  			this.bArtificalRootContext = false;
  			// Note: aApplicationFilter is used by sap.ui.comp.smarttable.SmartTable
  			this.aApplicationFilter = this._convertDeprecatedFilterObjects(aFilters);
  			this.aControlFilter = undefined;
  			this.aSorter = aSorter ? aSorter : [];
  			this.aMaxAggregationLevel = [];
  			this.aAggregationLevel = [];
  			this.oPendingRequests = {};
  			this.oPendingRequestHandle = [];
  			this.oGroupedRequests = {};
  			this.bUseBatchRequests = (mParameters && mParameters.useBatchRequests === true) ? true : false;
  			this.bProvideTotalSize = (mParameters && mParameters.provideTotalResultSize === false) ? false : true;
  			this.bProvideGrandTotals = (mParameters && mParameters.provideGrandTotals === false) ? false : true;
  			this.bReloadSingleUnitMeasures = (mParameters && mParameters.reloadSingleUnitMeasures === false) ? false : true;
  			this.bUseAcceleratedAutoExpand = (mParameters && mParameters.useAcceleratedAutoExpand === false) ? false : true;
  			this.bNoPaging = (mParameters && mParameters.noPaging === true) ? true : false;
  
  			// attribute members for maintaining loaded data; mapping from groupId to related information
  			this.iTotalSize = -1;
  				/* data loaded from OData service */
  			this.mServiceKey = {}; // keys of loaded entities belonging to group with given ID
  			this.mServiceLength = {}; // number of currently loaded entities
  			this.mServiceFinalLength = {}; // true iff all entities of group with given ID have been loaded (keys in mServiceKey)
  				/* consolidated view on loaded data */
  			this.mKeyIndex = {}; // consumer view: group entries are index positions in mServiceKey
  			this.mFinalLength = this.mServiceFinalLength; // true iff all entities of group with given ID have been loaded (keys in mKey)
  			this.mLength = {}; // number of currently loaded entities
  				/* locally created multi-currency entities */
  			this.mMultiUnitKey = {}; // keys of multi-currency entities
  			this.aMultiUnitLoadFactor = {}; // compensate discarded multi-unit entities by a load factor per aggregation level to increase number of loaded entities
  			this.bNeedsUpdate = false;
  			// use this.aSorter to sort the groups (only for non multi-unit cases)
  			this.bApplySortersToGroups = true;
  			// Content of this._autoExpandMode during last call of _canApplySortersToGroups;
  			// used for logging a warning if auto expand mode is bundled
  			this.sLastAutoExpandMode = undefined;
  			/* entity keys of loaded group Id's */
  			this.mEntityKey = {};
  			/* increased load factor due to ratio of non-multi-unit entities versus loaded entities */
  
  			// custom parameters which will be send with every request
  			// the custom parameters are extracted from the mParameters object, because the SmartTable does some weird things to the parameters
  			this.sCustomParams = this.oModel.createCustomParams({custom: this.mParameters.custom});
  
  			// attribute members for maintaining structure details requested by the binding consumer
  			this.oAnalyticalQueryResult = null; //will be initialized via the "initialize" function of the binding
  
  			this.aAnalyticalInfo = [];
  			this.mAnalyticalInfoByProperty = {};
  
  			// maintaining request to be bundled in a single $batch request
  			this.aBatchRequestQueue = [];
  
  			// considering different count mode settings
  			if (mParameters && mParameters.countMode == CountMode.None) {
  				oLogger.fatal("requested count mode is ignored; OData requests will include $inlinecout options");
  			} else if (mParameters
  					&& (mParameters.countMode == CountMode.Request
  						|| mParameters.countMode == CountMode.Both)) {
  				oLogger.warning("default count mode is ignored; OData requests will include $inlinecout options");
  			} else if (this.oModel.sDefaultCountMode == CountMode.Request) {
  				oLogger.warning("default count mode is ignored; OData requests will include $inlinecout options");
  			}
  
  			// detect ODataModel version
  			this.iModelVersion = AnalyticalVersionInfo.getVersion(this.oModel);
  			if (this.iModelVersion === null) {
  				oLogger.error("The AnalyticalBinding does not support Models other than sap.ui.model.odata.ODataModel version 1 or 2.");
  				return;
  			}
  
  			// list of sorted dimension names as basis for later calculations, initialized via "initialize" function
  			this.aAllDimensionSortedByName = null;
  
  			//Some setup steps have to be deferred, until the metadata was loaded by the model:
  			// - updateAnalyticalInfo, the parameters given in the constructor are kept though
  			// - fetch the oAnalyticalQueryResult
  			this.aInitialAnalyticalInfo = (mParameters == undefined ? [] : mParameters.analyticalInfo);
  
  			//this flag indicates if the analytical binding was initialized via initialize(), called either via bindAggregation or the Model
  			this.bInitial = true;
  		}
  
  	});
  
  
  	// Creates Information for SupportTool (see e.g. library.support.js of sap.ui.table library)
  	function createSupportInfo(oAnalyticalBinding, sErrorId) {
  		return function() {
  			if (!oAnalyticalBinding.__supportUID) {
  				oAnalyticalBinding.__supportUID = uid();
  			}
  			return {
  				type: sClassName,
  				analyticalError: sErrorId,
  				analyticalBindingId: oAnalyticalBinding.__supportUID
  			};
  		};
  	}
  
  
  	/**
  	 * Setter for context
  	 * @param {Object} oContext the new context object
  	 */
  	AnalyticalBinding.prototype.setContext = function (oContext) {
  		var sResolvedPath;
  
  		if (this.oContext !== oContext) {
  			this.oContext = oContext;
  
  			if (!this.isRelative()) {
  				// If binding is not a relative binding, nothing to do here
  				return;
  			}
  
  			this.oDataState = null;
  			this.bApplySortersToGroups = true;
  			// resolving the path makes sure that we can safely analyze the metadata,
  			// as we have a resourcepath for the QueryResult
  			sResolvedPath = this.oModel.resolve(this.sPath, this.oContext);
  			if (sResolvedPath) {
  				this.resetData();
  				this._initialize(); // triggers metadata/annotation check
  				this._fireChange({ reason: ChangeReason.Context });
  			} else {
  				this.bInitial = true;
  			}
  		}
  	};
  
  	/**
  	 * Initialize binding. Fires a change if data is already available ($expand) or a refresh.
  	 * If metadata is not yet available, do nothing, method will be called again when
  	 * metadata is loaded.
  	 *
  	 * The ODataModel will call this on all bindings as soon as the metadata was loaded
  	 *
  	 * @public
  	 * @name sap.ui.model.analytics.v2.AnalyticalBinding#initialize
  	 * @function
  	 */
  	AnalyticalBinding.prototype.initialize = function() {
  		if (this.oModel.oMetadata && this.oModel.oMetadata.isLoaded() && this.isInitial()) {
  
  			// relative bindings will be properly initialized once the context is set
  			var bIsRelative = this.isRelative();
  			if (!bIsRelative || (bIsRelative && this.oContext)) {
  				this._initialize();
  			}
  
  			this._fireRefresh({reason: ChangeReason.Refresh});
  		}
  		return this;
  	};
  
  	/**
  	 * Performs the actual initialization.
  	 * Called either by sap.ui.model.analytics.v2.AnalyticalBinding#initialize or
  	 * sap.ui.model.analytics.v2.AnalyticalBinding#setContext.
  	 */
  	AnalyticalBinding.prototype._initialize = function() {
  		if (this.oModel.oMetadata && this.oModel.oMetadata.isLoaded()) {
  			this.bInitial = false;
  			//first fetch the analyticalQueryResult object from the adapted Model (see ODataModelAdapter.js)
  			this.oAnalyticalQueryResult = this.oModel.getAnalyticalExtensions().findQueryResultByName(this._getEntitySet());
  
  			// Sanity check: If the AnalyticalQueryResult could not be retrieved, the AnalyticalBinding will not work correctly,
  			// and it will sooner or later break when accessing the AnalyticalQueryResult object.
  			if (!this.oAnalyticalQueryResult) {
  				throw ("Error in AnalyticalBinding - The QueryResult '" + this._getEntitySet() + "' could not be retrieved. Please check your service definition.");
  			}
  
  			//afterwards update the analyticalInfo with the initial parameters given in the constructor
  			this.updateAnalyticalInfo(this.aInitialAnalyticalInfo);
  			//initialize the list of sorted dimension names
  			this.aAllDimensionSortedByName = this.oAnalyticalQueryResult.getAllDimensionNames().concat([]).sort();
  
  			this._fireRefresh({reason: ChangeReason.Refresh});
  		}
  	};
  
  	/* *******************************
  	 *** API - Public methods
  	 ********************************/
  
  	/**
  	 * Gets the context for the root aggregation level representing the grand total for all bound measure properties.
  	 *
  	 * The context is assigned to parent group ID <code>null</code>. If the binding is configured not to provide a grand total,
  	 * this context is empty. If data for this context is not locally available yet, an OData request will be triggered to load it.
  	 *
  	 * This function must be called whenever the bound set of OData entities changes, e.g., by changing selected dimensions,
  	 * modifying filter conditions, etc.
  	 *
  	 * @function
  	 * @name sap.ui.model.analytics.AnalyticalBinding.prototype.getRootContexts
  	 * @param {map}
  	 *            mParameters specifying how the top-most aggregation level shall be fetched. Supported parameters are:
  	 * <ul>
  	 * <li>numberOfExpandedLevels: number of child levels that shall be fetched automatically</li>
  	 * <li>startIndex: index of first entry to return from parent group ID <code>"/"</code> (zero-based)</li>
  	 * <li>length: number of entries to return at and after the given start index</li>
  	 * <li>threshold: number of additional entries that shall be locally available in the binding for subsequent
  	 * accesses to contexts of parent group ID <code>"/"</code> or below, if auto-expanding is selected</li>
  	 * </ul>
  	 * @return {array}
  	 *            Array with a single object of class sap.ui.model.Context for the root context,
  	 *            or an empty array if an OData request is pending to fetch requested contexts that are not yet locally available.
  	 *
  	 * @public
  	 */
  	AnalyticalBinding.prototype.getRootContexts = function(mParameters) {
  
  		if (this.isInitial()) {
  			return [];
  		}
  
  		var iAutoExpandGroupsToLevel = (mParameters && mParameters.numberOfExpandedLevels ? mParameters.numberOfExpandedLevels + 1 : 1);
  // 		this._trace_enter("API", "getRootContexts", "", mParameters, ["numberOfExpandedLevels", "startIndex","length","threshold"]); // DISABLED FOR PRODUCTION
  		var aRootContext = null;
  
  		var sRootContextGroupMembersRequestId = this._getRequestId(AnalyticalBinding._requestType.groupMembersQuery, {groupId: null});
  
  		// if the root context is artificial (i.e. no grand total requested), then delay its return until all other related requests have been completed
  		if (this.bArtificalRootContext
  				&& !this._cleanupGroupingForCompletedRequest(sRootContextGroupMembersRequestId)) {
  // 			this._trace_leave("API", "getRootContexts", "delay until related requests have been completed"); // DISABLED FOR PRODUCTION
  			return aRootContext;
  		}
  		aRootContext = this._getContextsForParentContext(null);
  		if (aRootContext.length == 1) {
  // 			this._trace_leave("API", "getRootContexts", "", aRootContext, ["length"]); // DISABLED FOR PRODUCTION
  			return aRootContext;
  		}
  
  		if (iAutoExpandGroupsToLevel <= 1) {
  			if (iAutoExpandGroupsToLevel == 1) {
  				this._considerRequestGrouping([ sRootContextGroupMembersRequestId,
  												this._getRequestId(AnalyticalBinding._requestType.groupMembersQuery, {groupId: "/"}) ]);
  				this.getNodeContexts(this.getModel().getContext("/"), {
  					startIndex : mParameters.startIndex,
  					length : mParameters.length,
  					threshold : mParameters.threshold,
  					level : 0,
  					numberOfExpandedLevels : 0
  				});
  			}
  		} else {
  			var aRequestId = this._prepareGroupMembersAutoExpansionRequestIds("/", mParameters.numberOfExpandedLevels);
  			aRequestId.push(sRootContextGroupMembersRequestId);
  			this._considerRequestGrouping(aRequestId);
  			this.getNodeContexts(this.getModel().getContext("/"), {
  				startIndex : mParameters.startIndex,
  				length : mParameters.length,
  				threshold : mParameters.threshold,
  				level : 0,
  				numberOfExpandedLevels : mParameters.numberOfExpandedLevels
  			});
  /*			oLogger.fatal("not yet implemented: number of initially expanded levels may be 0 or 1, but not "
  					+ mParameters.numberOfExpandedLevels);
  */
  		}
  		if (aRootContext.length > 1) {
  			oLogger.fatal("assertion failed: grand total represented by a single entry");
  		}
  // 		this._trace_leave("API", "getRootContexts", "", aRootContext, ["length"]); // DISABLED FOR PRODUCTION
  		return aRootContext;
  	};
  
  	/**
  	 * Gets child contexts for a specified parent context.
  	 *
  	 * Contexts are returned in a stable order imposed by the
  	 * dimension property that defines this aggregation level beneath the parent context: Either a sort order has been specified for this property,
  	 * or the entries are returned in ascending order of the values of this dimension property by default.
  	 *
  	 * If any of the requested data is missing, an OData request will be triggered to load it.
  	 *
  	 * @function
  	 * @name sap.ui.model.analytics.AnalyticalBinding.prototype.getNodeContexts
  	 * @param {map}
  	 *            mParameters specifying the aggregation level for which contexts shall be fetched. Supported parameters are:
  	 * <ul>
  	 * <li>oContext: parent context identifying the requested group of child contexts</li>
  	 * <li>level: level number for oContext, because it might occur at multiple levels; context with group ID <code>"/"</code> has level 0</li>
  	 * <li>numberOfExpandedLevels: number of child levels that shall be fetched automatically</li>
  	 * <li>startIndex: index of first child entry to return from the parent context (zero-based)</li>
  	 * <li>length: number of entries to return; counting begins at the given start index</li>
  	 * <li>threshold: number of additional entries that shall be locally available in the binding for subsequent
  	 * accesses to child entries of the given parent context. </li>
  	 * </ul>
  	 * @return {array}
  	 *            Array containing the requested contexts of class sap.ui.model.Context, limited by the number of entries contained
  	 *            in the entity set at that aggregation level.
  	 *            The array will contain less than the requested number of contexts, if some are not locally available and an OData request is
  	 *            pending to fetch them. In this case, if the parameter numberOfExpandedLevels > 0, the array will be completely empty.
  	 *
  	 * @public
  	 */
  	AnalyticalBinding.prototype.getNodeContexts = function(oContext, mParameters) {
  
  		if (this.isInitial()) {
  			return [];
  		}
  
  // 		this._trace_enter("API", "getNodeContexts", "groupId=" + this._getGroupIdFromContext(oContext, mParameters.level), mParameters,["startIndex","length","threshold"]); // DISABLED FOR PRODUCTION
  		var iStartIndex, iLength, iThreshold, iLevel, iNumberOfExpandedLevels, bSupressRequest;
  		if (typeof mParameters == "object") {
  			iStartIndex = mParameters.startIndex;
  			iLength = mParameters.length;
  			iThreshold = mParameters.threshold;
  			iLevel = mParameters.level;
  			iNumberOfExpandedLevels = mParameters.numberOfExpandedLevels;
  			bSupressRequest = mParameters.supressRequest;
  		} else { // due to compatibility; can be removed if table is adapted
  			iStartIndex = arguments[1];
  			iLength = arguments[2];
  			iThreshold = arguments[3];
  			iLevel = arguments[4];
  			iNumberOfExpandedLevels = arguments[5];
  			bSupressRequest = arguments[6];
  		}
  
  		var aContext = this._getContextsForParentContext(oContext, iStartIndex, iLength, iThreshold, iLevel, iNumberOfExpandedLevels, bSupressRequest);
  // 		this._trace_leave("API", "getNodeContexts", "", aContext, ["length"]); // DISABLED FOR PRODUCTION
  		return aContext;
  	};
  
  	AnalyticalBinding.prototype.ContextsAvailabilityStatus = { ALL: 2, SOME: 1, NONE: 0 };
  	/**
  	 * Determines if the binding has the entries of a given aggregation level locally available.
  	 *
  	 * If so, no further OData request is required to fetch any of them.
  	 * @function
  	 * @name sap.ui.model.analytics.AnalyticalBinding.prototype.hasAvailableNodeContexts
  	 * @param {sap.ui.model.Context}
  	 *            oContext the parent context identifying the aggregation level.
  	 * @param {int}
  	 *            iLevel the level number of oContext (because the context might occur at multiple levels).
  	 * @return {boolean}
  	 *            property of sap.ui.model.analytics.AnalyticalBinding.ContextsAvailabilityStatus,
  	 * indicating whether all, some, or none of the entries are locally available.
  	 * @public
  	 */
  	AnalyticalBinding.prototype.hasAvailableNodeContexts = function(oContext, iLevel) {
  		var sGroupId = this._getGroupIdFromContext(oContext, iLevel);
  		if (this._getKeys(sGroupId) != undefined) {
  			if (this.mFinalLength[sGroupId] == true) {
  				return AnalyticalBinding.prototype.ContextsAvailabilityStatus.ALL;
  			} else {
  				return AnalyticalBinding.prototype.ContextsAvailabilityStatus.SOME;
  			}
  		} else {
  			return AnalyticalBinding.prototype.ContextsAvailabilityStatus.NONE;
  		}
  	};
  
  	/**
  	 * Gets the total number of contexts contained in a group, if known.
  	 *
  	 * For a given group, be aware that the group size might vary over time. In principle, this can happen if the
  	 * bound set of OData entities includes measure properties with amount or quantity values. The AnalyticalBinding
  	 * recognizes situations where the OData service returns multiple entries for a single group entry due to the fact that a
  	 * measure property cannot be aggregated properly, because an amount exists in multiple currencies or a quantity exists
  	 * in multiple units. In such situations, the AnalyticalBinding substitutes these entries by a single representative, and
  	 * the group size gets reduced by the count of duplicate entries. Finally, since the Binding does not always fetch all children of
  	 * a group at once, but only a page with a certain range, such size changes might happen after every page access.
  	 * @function
  	 * @name sap.ui.model.analytics.AnalyticalBinding.prototype.getGroupSize
  	 * @param {sap.ui.model.Context}
  	 *            oContext the parent context identifying the requested group of child contexts.
  	 * @param {int}
  	 *            iLevel the level number of oContext (because the context might occur at multiple levels)
  	 * @return {int}
  	 *            The currently known group size, or -1, if not yet determined
  	 * @public
  	 */
  	AnalyticalBinding.prototype.getGroupSize = function(oContext, iLevel) {
  		if (oContext === undefined) {
  			return 0; // API robustness
  		}
  		var sGroupId = this._getGroupIdFromContext(oContext, iLevel);
  
  		return this.mFinalLength[sGroupId] ? this.mLength[sGroupId] : -1;
  	};
  
  	/**
  	 * Gets the total number of entities in the bound OData entity set.
  	 *
  	 * Counting takes place at the lowest aggregation level defined by the possible value combinations for the complete set of
  	 * dimension properties included in the bound entity set. This means that intermediate aggregate entities with
  	 * sub-totals at higher aggregation levels are not counted.
  	 *
  	 * @function
  	 * @name sap.ui.model.analytics.AnalyticalBinding.prototype.getTotalSize
  	 * @return {int}
  	 *            the total number of addressed entities in the OData entity set
  	 *
  	 * @public
  	 */
  	AnalyticalBinding.prototype.getTotalSize = function() {
  		if (!this.bProvideTotalSize) {
  			oLogger.fatal("total size of result explicitly turned off, but getter invoked");
  		}
  		return +this.iTotalSize;
  	};
  
  	/**
  	 * Determines if the contexts in a specified group have further children. If so,
  	 * any of these group contexts can be a parent context of a nested sub-group in
  	 * a subsequent aggregation level.
  	 *
  	 * @function
  	 * @name sap.ui.model.analytics.AnalyticalBinding.prototype.hasChildren
  	 * @param {sap.ui.model.Context}
  	 *            oContext the parent context identifying the requested group of child contexts.
  	 * @param {map}
  	 *            mParameters The only supported parameter is level as the level number of oContext (because the context might occur at multiple levels)
  	 * @return {boolean}
  	 *            true if and only if the contexts in the specified group have further children.
  	 * @public
  	 */
  	AnalyticalBinding.prototype.hasChildren = function(oContext, mParameters) {
  
  		if (oContext === undefined) {
  			return false; // API robustness
  		}
  		if (oContext == null) {
  			return true;
  		}
  		var iContextLevel = mParameters.level;
  		if (iContextLevel == 0) {
  			return true;
  		}
  
  		if (this.aAggregationLevel.length < iContextLevel) {
  			return false;
  		}
  		// children exist if it is not the rightmost grouped column or there is at least one further level with an ungrouped groupable column.
  		return this.aMaxAggregationLevel.indexOf(this.aAggregationLevel[iContextLevel - 1]) < this.aMaxAggregationLevel.length - 1;
  	};
  
  	/**
  	 * Determines if any of the properties included in the bound OData entity set is a measure property.
  	 *
  	 * @function
  	 * @name sap.ui.model.analytics.AnalyticalBinding.prototype.hasMeasures
  	 * @return {boolean}
  	 *            true if and only one or more properties are measure properties.
  	 *
  	 * @public
  	 */
  	AnalyticalBinding.prototype.hasMeasures = function() {
  		var bHasMeasures = false;
  		for (var p in this.oMeasureDetailsSet) {
  			if (this.oMeasureDetailsSet.hasOwnProperty(p)) {
  				bHasMeasures = true;
  				break;
  			}
  		}
  		return bHasMeasures;
  	};
  
  	/**
  	 * Gets details about the dimension properties included in the bound OData entity set.
  	 *
  	 * @function
  	 * @name sap.ui.model.analytics.AnalyticalBinding.prototype.getDimensionDetails
  	 * @return {map}
  	 *            details for every dimension property addressed by its name. The details object provides these properties: name of the dimension,
  	 * keyPropertyName for the name of the property holding the dimension key, textPropertyName for the name of the property holding the
  	 * text for the dimension, aAttributeName listing all properties holding dimension attributes, grouped as indicator whether or not this
  	 * dimension is currently grouped, and analyticalInfo, which contains the binding information for this dimension passed from the
  	 * AnalyticalBinding's consumer via call to function updateAnalyticalInfo.
  	 *
  	 * @public
  	 */
  	AnalyticalBinding.prototype.getDimensionDetails = function() {
  		return this.oDimensionDetailsSet;
  	};
  
  	/**
  	 * Gets details about the measure properties included in the bound OData entity set.
  	 *
  	 * @function
  	 * @name sap.ui.model.analytics.AnalyticalBinding.prototype.getMeasureDetails
  	 * @return {map}
  	 *            details for every measure property addressed by its name. The details object provides these properties: name of the measure,
  	 * rawValuePropertyName for the name of the property holding the raw value, unitPropertyName for the name of the property holding the related
  	 * value unit or currency, if any, and analyticalInfo, which contains the binding information for this measure passed from the
  	 * AnalyticalBinding's consumer via call to function updateAnalyticalInfo.
  	 *
  	 * @public
  	 */
  	AnalyticalBinding.prototype.getMeasureDetails = function() {
  		return this.oMeasureDetailsSet;
  	};
  
  	/**
  	 * Determines if the binding has been configured to provide a grand total for the selected measure properties.
  	 *
  	 * @function
  	 * @name sap.ui.model.analytics.AnalyticalBinding.prototype.providesGrandTotal
  	 * @return {boolean}
  	 *            true if and only if the binding provides a context for the grand totals of all selected measure properties.
  	 *
  	 * @public
  	 */
  	AnalyticalBinding.prototype.providesGrandTotal = function() {
  		return this.bProvideGrandTotals;
  	};
  
  	/**
  	 * Gets the metadata of a property with a given name.
  	 *
  	 * @function
  	 * @name sap.ui.model.analytics.AnalyticalBinding.prototype.getProperty
  	 * @param {string}
  	 *            sPropertyName The property name.
  	 * @return {object}
  	 *            OData metadata of this property or null if it does not exist.
  	 * @public
  	 */
  	AnalyticalBinding.prototype.getProperty = function(sPropertyName) {
  		if (this.isInitial()) {
  			return {};
  		}
  		return this.oAnalyticalQueryResult.getEntityType().findPropertyByName(sPropertyName);
  	};
  
  	/**
  	 * Gets the names of the filterable properties in the bound OData entity set.
  	 *
  	 * @function
  	 * @name sap.ui.model.analytics.AnalyticalBinding.prototype.getFilterablePropertyNames
  	 * @returns {array}
  	 *             names of properties that can be filtered.
  	 *
  	 * @public
  	 */
  	AnalyticalBinding.prototype.getFilterablePropertyNames = function() {
  		if (this.isInitial()) {
  			return [];
  		}
  		return this.oAnalyticalQueryResult.getEntityType().getFilterablePropertyNames();
  	};
  
  	/**
  	 * Gets the names of the sortable properties in the bound OData entity set.
  	 *
  	 * @function
  	 * @name sap.ui.model.analytics.AnalyticalBinding.prototype.getSortablePropertyNames
  	 * @returns {array}
  	 *             names of properties that can be used for sorting the result entities.
  	 *
  	 * @public
  	 */
  	AnalyticalBinding.prototype.getSortablePropertyNames = function() {
  		if (this.isInitial()) {
  			return [];
  		}
  		return this.oAnalyticalQueryResult.getEntityType().getSortablePropertyNames();
  	};
  
  	/**
  	 * Gets the label of a property with a given name.
  	 *
  	 * @function
  	 * @name sap.ui.model.analytics.AnalyticalBinding.prototype.getPropertyLabel
  	 * @param {string}
  	 *            sPropertyName The property name.
  	 * @returns {string}
  	 *            The label maintained for this property or null if it does not exist.
  	 *
  	 * @public
  	 */
  	AnalyticalBinding.prototype.getPropertyLabel = function(sPropertyName) {
  		if (this.isInitial()) {
  			return "";
  		}
  		return this.oAnalyticalQueryResult.getEntityType().getLabelOfProperty(sPropertyName);
  	};
  
  	/**
  	 * Gets the label of a property with a given name.
  	 *
  	 * @function
  	 * @name sap.ui.model.analytics.AnalyticalBinding.prototype.getPropertyHeading
  	 * @param {string}
  	 *            sPropertyName The property name.
  	 * @returns {string}
  	 *            The heading maintained for this property or null if it does not exist.
  	 *
  	 * @public
  	 */
  	AnalyticalBinding.prototype.getPropertyHeading = function(sPropertyName) {
  		if (this.isInitial()) {
  			return "";
  		}
  		return this.oAnalyticalQueryResult.getEntityType().getHeadingOfProperty(sPropertyName);
  	};
  
  	/**
  	 * Gets the quick info of a property with a given name.
  	 *
  	 * @function
  	 * @name sap.ui.model.analytics.AnalyticalBinding.prototype.getPropertyQuickInfo
  	 * @param {string}
  	 *            sPropertyName The property name.
  	 * @returns {string}
  	 *            The quick info maintained for this property or null if it does not exist.
  	 *
  	 * @public
  	 */
  	AnalyticalBinding.prototype.getPropertyQuickInfo = function(sPropertyName) {
  		if (this.isInitial()) {
  			return "";
  		}
  		return this.oAnalyticalQueryResult.getEntityType().getQuickInfoOfProperty(sPropertyName);
  	};
  
  	/**
  	 * Determines if a given name refers to a measure property
  	 *
  	 * @function
  	 * @name sap.ui.model.analytics.AnalyticalBinding.prototype.isMeasure
  	 * @param {string}
  	 *            sPropertyName The property name.
  	 * @return {boolean}
  	 *            true if and only if the bound OData entity set includes a measure property with this name.
  	 *
  	 * @public
  	 */
  	AnalyticalBinding.prototype.isMeasure = function(sPropertyName) {
  		return this.aMeasureName && this.aMeasureName.indexOf(sPropertyName) !== -1;
  	};
  
  	/**
  	 * Sets filters for matching only a subset of the entities in the bound OData entity set.
  	 *
  	 * Invoking this function resets the state of the binding. Subsequent data requests such as calls to getNodeContexts() will
  	 * need to trigger OData requests in order to fetch the data that are in line with these filters.
  	 *
  	 * @function
  	 * @name sap.ui.model.analytics.AnalyticalBinding.prototype.filter
  	 * @param {sap.ui.model.Filter[]|sap.ui.model.Filter}
  	 *            aFilter an Array of sap.ui.model.Filter objects or a single Filter instance.
  	 * @param {sap.ui.model.FilterType}
  	 *            [sFilterType=sap.ui.model.FilterType.Control] Type of the filter which should be adjusted.
  	 * @return {sap.ui.model.analytics.AnalyticalBinding}
  	 *            returns <code>this</code> to facilitate method chaining
  	 *
   	 * @public
  	 */
  	AnalyticalBinding.prototype.filter = function(aFilter, sFilterType) {
  		//ensure at least an empty array, so the later validation of odata4analytics.js does not fail
  		if (!aFilter) {
  			aFilter = [];
  		}
  		// wrap filter argument in an array if it's a single instance
  		if (aFilter instanceof Filter) {
  			aFilter = [aFilter];
  		}
  
  		aFilter = this._convertDeprecatedFilterObjects(aFilter);
  
  		if (sFilterType == FilterType.Application) {
  			this.aApplicationFilter = aFilter;
  		} else {
  			this.aControlFilter = aFilter;
  		}
  
  		this.iTotalSize = -1; // invalidate last row counter
  
  		this._abortAllPendingRequests();
  
  		this.resetData();
  		// resets the flag to sort groups by this.aSorter; a new filter might resolve a multi-unit
  		// case; do it before refresh event is fired
  		this.bApplySortersToGroups = true;
  		this._fireRefresh({
  			reason : ChangeReason.Filter
  		});
  
  		return this;
  	};
  
  	/**
  	 * Returns the filter information as an abstract syntax tree.
  	 * Consumers must not rely on the origin information to be available, future filter
  	 * implementations will not provide this information.
  	 *
  	 * @param {boolean} [bIncludeOrigin=false] whether to include information about the filter
  	 *   objects from which the tree has been created
  	 * @returns {object} The AST of the filter tree or null if no filters are set
  	 * @private
  	 * @ui5-restricted sap.ui.table, sap.ui.export
  	 */
  	//@override
  	AnalyticalBinding.prototype.getFilterInfo = function(bIncludeOrigin) {
  		var oCombinedFilter = FilterProcessor.combineFilters(this.aControlFilter,
  				this.aApplicationFilter);
  
  		if (oCombinedFilter) {
  			return oCombinedFilter.getAST(bIncludeOrigin);
  		}
  
  		return null;
  	};
  
  	/**
  	 * Sets sorters for retrieving the entities in the bound OData entity set in a specific order.
  	 *
  	 * Invoking this function resets the state of the binding. Subsequent data requests such as calls to getNodeContexts() will
  	 * need to trigger OData requests in order to fetch the data that are in line with these sorters.
  	 *
  	 * @function
  	 * @name sap.ui.model.analytics.AnalyticalBinding.prototype.sort
  	 * @param {sap.ui.model.Sorter|array}
  	 *            aSorter a sorter object or an array of sorter objects which define the sort order.
  	 * @return {sap.ui.model.analytics.AnalyticalBinding}
  	 *            returns <code>this</code> to facilitate method chaining.
  	 *
  	 * @public
  	 */
  	AnalyticalBinding.prototype.sort = function(aSorter) {
  
  		if (aSorter instanceof Sorter) {
  			aSorter = [ aSorter ];
  		}
  
  		this.aSorter = aSorter ? aSorter : [];
  
  		this._abortAllPendingRequests();
  		this.resetData(undefined, {reason: ChangeReason.Sort});
  		this._fireRefresh({
  			reason : ChangeReason.Sort
  		});
  
  		return this;
  	};
  
  	/**
  	 * Gets a printable name for a group.
  	 *
  	 * The printable name follows the pattern is <code>&lt;label&gt;:&lt;key-value&gt;[-&lt;text-value&gt;]</code>,
  	 * where <code>label</code> is the label of the dimension property used at the aggregation level for the group,
  	 * <code>key-value</code> is the key value of that dimension for the group, and <code>text-value</code> is the
  	 * value of the associated text property, if it is also used in the binding.
  	 *
  	 * Whenever a formatter function has been defined for a column displaying the key or text of this dimension, the return value
  	 * of this function is applied for the group name instead of the respective key or text value.
  	 *
  	 * @function
  	 * @name sap.ui.model.analytics.AnalyticalBinding.prototype.getGroupName
  	 * @param {sap.ui.model.Context}
  	 *            oContext the parent context identifying the requested group.
  	 * @param {int}
  	 *            iLevel the level number of oContext (because the context might occur at multiple levels)
  	 * @return {string} a printable name for the group.
  	 * @public
  	 */
  	AnalyticalBinding.prototype.getGroupName = function(oContext, iLevel) {
  		if (oContext === undefined) {
  			return ""; // API robustness
  		}
  
  		var sGroupProperty = this.aAggregationLevel[iLevel - 1],
  			oDimension = this.oAnalyticalQueryResult.findDimensionByPropertyName(sGroupProperty),
  			// it might happen that grouped property is not contained in the UI (e.g. if grouping is
  			// done with a dimension's text property)
  			fValueFormatter = this.mAnalyticalInfoByProperty[sGroupProperty]
  				&& this.mAnalyticalInfoByProperty[sGroupProperty].formatter,
  			sPropertyValue = oContext.getProperty(sGroupProperty),
  			oTextProperty, sFormattedPropertyValue, sGroupName;
  
  		if (oDimension && this.oDimensionDetailsSet[sGroupProperty].textPropertyName) {
  			oTextProperty = oDimension.getTextProperty();
  		}
  
  		var sTextProperty, sTextPropertyValue, fTextValueFormatter;
  		if (oTextProperty) {
  			sTextProperty = oDimension.getTextProperty().name;
  			// it might happen that text property is not contained in the UI
  			fTextValueFormatter = this.mAnalyticalInfoByProperty[sTextProperty]
  				&& this.mAnalyticalInfoByProperty[sTextProperty].formatter;
  			sTextPropertyValue = oContext.getProperty(sTextProperty);
  		}
  
  		if (!oTextProperty) {
  			sFormattedPropertyValue = fValueFormatter ? fValueFormatter(sPropertyValue) : sPropertyValue;
  			sGroupName = (oDimension.getLabelText ? oDimension.getLabelText() + ': ' : '')
  				+ sFormattedPropertyValue;
  		} else {
  			sFormattedPropertyValue = fValueFormatter ? fValueFormatter(sPropertyValue, sTextPropertyValue) : sPropertyValue;
  			sGroupName = (oDimension.getLabelText ? oDimension.getLabelText() + ': ' : '')
  				+ sFormattedPropertyValue;
  
  			var sFormattedTextPropertyValue = fTextValueFormatter ? fTextValueFormatter(sTextPropertyValue, sPropertyValue) : sTextPropertyValue;
  			if (sFormattedTextPropertyValue) {
  				sGroupName += ' - ' + sFormattedTextPropertyValue;
  			}
  		}
  
  		return sGroupName;
  	};
  
  	/**
  	 * Updates the binding's structure with new analytical information.
  	 *
  	 * Analytical information is the mapping of UI columns to properties in the bound OData entity set. Every column object contains
  	 * the name of the bound property and in addition:
  	 * <ol>
  	 *   <li>A column bound to a dimension property has further boolean properties:
  	 *     <ul>
  	 *       <li>grouped: dimension will be used for building groups</li>
  	 *       <li>visible: if the column is visible, values for the related property will be fetched from the OData service</li>
  	 *       <li>inResult: if the column is not visible, but declared to be part of the result, values for the related property
  	 *       will also be fetched from the OData service</li>
  	 *     </ul>
  	 *   </li>
  	 *   <li>A column bound to a measure property has further boolean properties:
  	 *     <ul>
  	 *       <li>total: totals and sub-totals will be provided for the measure at all aggregation levels</li>
  	 *     </ul>
  	 *   </li>
  	 *   <li>A column bound to a hierarchy property has further properties:
  	 *     <ul>
  	 *       <li>grouped: boolean value; indicates whether the hierarchy will be used for building
  	 *           groups</li>
  	 *       <li>level: integer value; the hierarchy level is mandatory for at least one of those
  	 *           columns that represent the same hierarchy.</li>
  	 *     </ul>
  	 *   </li>
  	 * </ol>
  	 *
  	 * Invoking this function resets the state of the binding and subsequent data requests such as calls to getNodeContexts() will
  	 * need to trigger OData requests in order to fetch the data that are in line with this analytical information.
  	 *
  	 * Please be aware that a call of this function might lead to additional back-end requests, as well as a control re-rendering later on.
  	 * Whenever possible use the API of the analytical control, instead of relying on the binding.
  	 *
  	 * @function
  	 * @name sap.ui.model.analytics.AnalyticalBinding.prototype.updateAnalyticalInfo
  	 * @param {array}
  	 *            aColumns an array with objects holding the analytical information for every column, from left to right.
  	 * @protected
  	 */
  	AnalyticalBinding.prototype.updateAnalyticalInfo = function(aColumns, bForceChange) {
  		var oDimensionDetails,
  			oEntityType,
  			aHierarchyProperties,
  			that = this;
  
  		/*
  		 * If the given analytical column is related to a hierarchy, add or update the corresponding
  		 * entry in <code>that.mHierarchyDetailsByName</code>.
  		 * @param {object} The analytical info for an analytical column
  		 */
  		function addOrUpdateHierarchy(oColumn) {
  			var iLevel = oColumn.level,
  				sName = oColumn.name;
  
  			aHierarchyProperties = aHierarchyProperties
  				|| oEntityType.getAllHierarchyPropertyNames();
  
  			aHierarchyProperties.forEach(function (sHierarchyName) {
  				var oHierarchy = that.oAnalyticalQueryResult
  						.findDimensionByPropertyName(sHierarchyName).getHierarchy(),
  					oHierarchyDetails = null,
  					// each hierarchy has a node ID property, see post processing in
  					// sap.ui.model.analytics.odata4analytics.EntityType.prototype._init
  					sNodeIDName = oHierarchy.getNodeIDProperty().name,
  					oProperty;
  
  				if (sNodeIDName === sName) {
  					oHierarchyDetails = getOrCreateHierarchyDetails(oHierarchy);
  				} else {
  					oProperty = oHierarchy.getNodeExternalKeyProperty();
  					if (oProperty && oProperty.name === sName) {
  						oHierarchyDetails = getOrCreateHierarchyDetails(oHierarchy);
  						oHierarchyDetails.nodeExternalKeyName = sName;
  					} else {
  						oProperty = oEntityType.getTextPropertyOfProperty(sNodeIDName);
  						if (oProperty && oProperty.name === sName) {
  							oHierarchyDetails = getOrCreateHierarchyDetails(oHierarchy);
  							oHierarchyDetails.nodeTextName = sName;
  						}
  					}
  				}
  				if (oHierarchyDetails && "level" in oColumn) {
  					// add level restriction and check that aColumns is properly defined
  					if (typeof iLevel === "number") {
  						if ("level" in oHierarchyDetails && oHierarchyDetails.level !== iLevel) {
  							throw new Error("Multiple different level filter for hierarchy '"
  								+ sNodeIDName + "' defined");
  						}
  						oHierarchyDetails.level = iLevel;
  						// the property which defines the level also defines the grouping
  						oHierarchyDetails.grouped = !!oColumn.grouped;
  					} else {
  						throw new Error("The level of '" + sNodeIDName
  							+ "' has to be an integer value");
  					}
  				}
  			});
  		}
  
  		/*
  		 * Get the hierarchy details for the given name from
  		 * <code>that.mHierarchyDetailsByName</code>. If there is no entry in the set, a new empty
  		 * object is added to the hierarchy details map and returned.
  		 * @param {object} oHierarchy The hierarchy for which to get the details
  		 * @returns {object} The hierarchy details object.
  		 */
  		function getOrCreateHierarchyDetails(oHierarchy) {
  			var sName = oHierarchy.getNodeIDProperty().name,
  				oNodeLevelProperty,
  				oHierarchyDetails = that.mHierarchyDetailsByName[sName];
  
  			if (!oHierarchyDetails) {
  				oNodeLevelProperty = oHierarchy.getNodeLevelProperty();
  				// add hierarchy information
  				oHierarchyDetails = {
  					dimensionName : oHierarchy.getNodeValueProperty().name,
  					nodeIDName : sName,
  					nodeLevelName : oNodeLevelProperty && oNodeLevelProperty.name
  				};
  				that.mHierarchyDetailsByName[sName] = oHierarchyDetails;
  			}
  			return oHierarchyDetails;
  		}
  
  		if (!this.oModel.oMetadata || !this.oModel.oMetadata.isLoaded() || this.isInitial()) {
  			this.aInitialAnalyticalInfo = aColumns;
  			return;
  		}
  
  		// check if something has changed --> deep equal on the column info objects, only 1 level "deep"
  		if (deepEqual(this._aLastChangedAnalyticalInfo, aColumns)) {
  			if (bForceChange) {
  				setTimeout(function () {
  					this._fireChange({reason: ChangeReason.Change});
  				}.bind(this), 0);
  			}
  			return;
  		}
  
  		oEntityType = this.oAnalyticalQueryResult.getEntityType();
  		// make a deep copy of the column definition, so we can ignore duplicate calls the next time, see above
  		// copy is necessary because the original analytical info will be changed and used internally, through out the binding "coding"
  		this._aLastChangedAnalyticalInfo = [];
  		for (var j = 0; j < aColumns.length; j++) {
  			this._aLastChangedAnalyticalInfo[j] = jQuery.extend({}, aColumns[j]);
  		}
  
  		// parameter is an array with elements whose structure is defined by sap.ui.analytics.model.AnalyticalTable.prototype._getColumnInformation()
  		var oPreviousDimensionDetailsSet = this.oDimensionDetailsSet || {},
  			oPreviousMeasureDetailsSet = this.oMeasureDetailsSet || {};
  
  		this.mAnalyticalInfoByProperty = {}; // enable associative access to analytical update information
  		this.aMaxAggregationLevel = []; // names of all dimensions referenced by any column
  		this.aAggregationLevel = []; // names of all currently grouped dimensions
  		this.aMeasureName = []; // names of all measures referenced by any column
  		if (this.iAnalyticalInfoVersionNumber == undefined) {
  			this.iAnalyticalInfoVersionNumber = 1;
  		} else if (this.iAnalyticalInfoVersionNumber > 999) {
  			this.iAnalyticalInfoVersionNumber = 1;
  		} else {
  			this.iAnalyticalInfoVersionNumber = this.iAnalyticalInfoVersionNumber + 1;
  		}
  
  		this.oMeasureDetailsSet = {}; // properties with structure {rawValueProperty,unitProperty,formattedValueProperty}
  		this.oDimensionDetailsSet = {}; // properties with structure {name,keyProperty,textProperty,aAttributeName}
  		this.aAdditionalSelects = [];
  		// Maps the nodeIDName to an object with the structure: {dimensionName, grouped, level,
  		// nodeExternalKeyName, nodeIDName, nodeLevelName, nodeTextName}
  		this.mHierarchyDetailsByName = {}; //
  
  		// process column settings for dimensions and measures part of the result or visible
  		for (var i = 0; i < aColumns.length; i++) {
  			// determine requested aggregation level from columns representing dimension-related properties
  			var oDimension = this.oAnalyticalQueryResult.findDimensionByPropertyName(aColumns[i].name);
  			if (oDimension && (aColumns[i].inResult == true || aColumns[i].visible == true)) {
  				aColumns[i].dimensionPropertyName = oDimension.getName();
  				oDimensionDetails = this.oDimensionDetailsSet[oDimension.getName()];
  				if (!oDimensionDetails) {
  					oDimensionDetails = {};
  					oDimensionDetails.name = oDimension.getName();
  					oDimensionDetails.aAttributeName = [];
  					oDimensionDetails.grouped = false;
  					this.oDimensionDetailsSet[oDimension.getName()] = oDimensionDetails;
  					this.aMaxAggregationLevel.push(oDimensionDetails.name);
  					if (aColumns[i].grouped == true) {
  						this.aAggregationLevel.push(oDimensionDetails.name);
  					}
  				}
  				if (aColumns[i].grouped == true) {
  					if (!this.getSortablePropertyNames() || this.getSortablePropertyNames().indexOf(oDimension.getName()) == -1) {
  						oLogger.fatal("property " + oDimension.getName() + " must be sortable in order to be used as grouped dimension");
  					}
  					oDimensionDetails.grouped = true;
  				}
  
  				if (oDimension.getName() == aColumns[i].name) {
  					oDimensionDetails.keyPropertyName = aColumns[i].name;
  				}
  				var oTextProperty = oDimension.getTextProperty();
  				if (oTextProperty && oTextProperty.name == aColumns[i].name) {
  					oDimensionDetails.textPropertyName = aColumns[i].name;
  				}
  				if (oDimension.findAttributeByName(aColumns[i].name)) {
  					oDimensionDetails.aAttributeName.push(aColumns[i].name);
  				}
  				oDimensionDetails.analyticalInfo = aColumns[i];
  			}
  
  			// determine necessary measure details from columns visualizing measure-related properties
  			var oMeasure = this.oAnalyticalQueryResult.findMeasureByPropertyName(aColumns[i].name);
  			if (oMeasure && (aColumns[i].inResult == true || aColumns[i].visible == true)) {
  				aColumns[i].measurePropertyName = oMeasure.getName();
  				var oMeasureDetails = this.oMeasureDetailsSet[oMeasure.getName()];
  				if (!oMeasureDetails) {
  					oMeasureDetails = {};
  					oMeasureDetails.name = oMeasure.getName();
  					this.oMeasureDetailsSet[oMeasure.getName()] = oMeasureDetails;
  					this.aMeasureName.push(oMeasureDetails.name);
  				}
  				if (oMeasure.getRawValueProperty().name == aColumns[i].name) {
  					oMeasureDetails.rawValuePropertyName = aColumns[i].name;
  				}
  				var oFormattedValueProperty = oMeasure.getFormattedValueProperty();
  				if (oFormattedValueProperty && oFormattedValueProperty.name == aColumns[i].name) {
  					oMeasureDetails.formattedValuePropertyName = aColumns[i].name;
  				}
  				oMeasureDetails.analyticalInfo = aColumns[i];
  			}
  
  			// determine requested hierarchy information from columns representing hierarchy-related
  			// information (column properties are not considered)
  			if (!oDimension && !oMeasure) {
  				addOrUpdateHierarchy(aColumns[i]);
  			}
  			this.mAnalyticalInfoByProperty[aColumns[i].name] = aColumns[i];
  		}
  		// for compatibility reasons remove hierarchy elements without a level information
  		Object.keys(this.mHierarchyDetailsByName).forEach(function (sNodeIDName) {
  			var oHierarchyDetails = that.mHierarchyDetailsByName[sNodeIDName];
  			if (!("level" in oHierarchyDetails)) {
  				delete that.mHierarchyDetailsByName[sNodeIDName];
  				if (oLogger.isLoggable(Log.Level.INFO)) {
  					oLogger.info("No level specified for hierarchy node '" + sNodeIDName
  						+ "'; ignoring hierarchy", "");
  				}
  			} else if (!that.oDimensionDetailsSet[sNodeIDName]) {
  				// also add it as regular dimension, which is a precondition to integrate
  				// hierarchies with regular processing of data requests and responses
  				that.oDimensionDetailsSet[sNodeIDName] = {
  					aAttributeName : [],
  					grouped : oHierarchyDetails.grouped,
  					isHierarchyDimension : true, // mark it as hierarchy dimension
  					name : sNodeIDName
  				};
  				that.aMaxAggregationLevel.push(sNodeIDName);
  				if (oHierarchyDetails.grouped) {
  					that.aAggregationLevel.push(sNodeIDName);
  				}
  			}
  		});
  
  		// finalize measure information with unit properties also being part of the table
  		for ( var measureName in this.oMeasureDetailsSet) {
  			var oUnitProperty = this.oAnalyticalQueryResult.findMeasureByName(measureName).getUnitProperty();
  			if (oUnitProperty) {
  				this.oMeasureDetailsSet[measureName].unitPropertyName = oUnitProperty.name;
  			}
  		}
  
  		// check if any dimension has been added or removed. If so, invalidate the total size
  		var bDimensionsChanged = Object.keys(oPreviousDimensionDetailsSet).sort().join(";")
  				!== Object.keys(this.oDimensionDetailsSet).sort().join(";");
  		if (bDimensionsChanged) {
  			this.iTotalSize = -1;
  		}
  		if (bDimensionsChanged
  				|| Object.keys(oPreviousMeasureDetailsSet).sort().join(";")
  					!== Object.keys(this.oMeasureDetailsSet).sort().join(";")) {
  			// do not reset the flag if the dimensions and the measures are the same
  			this.bApplySortersToGroups = true;
  		}
  
  		// remember column settings for later reference
  		this.aAnalyticalInfo = aColumns;
  
  		// reset attributes holding previously loaded data
  		this.resetData();
  
  		this.bNeedsUpdate = false;
  
  		if (this.mParameters.select) {
  			this.aAdditionalSelects = getAdditionalSelects(this);
  		}
  
  		if (bForceChange) {
  			this._fireChange({reason: ChangeReason.Change});
  		}
  
  	};
  
  	/**
  	 * Gets the analytical information for a column with a given name.
  	 *
  	 * @function
  	 * @name sap.ui.model.analytics.AnalyticalBinding.prototype.getAnalyticalInfoForColumn
  	 * @param {string} sColumnName the column name.
  	 * @return {object}
  	 *            analytical information for the column; see {@link #updateAnalyticalInfo}
  	 *            for an explanation of the object structure
  	 * @public
  	 */
  	AnalyticalBinding.prototype.getAnalyticalInfoForColumn = function(sColumnName) {
  		return this.mAnalyticalInfoByProperty[sColumnName];
  	};
  
  	/**
  	 * Loads child contexts of multiple groups.
  	 *
  	 * @function
  	 * @name sap.ui.model.analytics.AnalyticalBinding.prototype.loadGroups
  	 * @param {map}
  	 *            mGroupIdRanges specifies index ranges of child contexts to be loaded for multiple groups identified by their ID. A group index range is
  	 *            given by an object consisting of startIndex, length, threshold. For every group ID, the map holds an array of such range objects.
  	 *
  	 * @public
  	 */
  	AnalyticalBinding.prototype.loadGroups = function(mGroupIdRanges) {
  		var aGroupId = [];
  		for ( var sGroupId in mGroupIdRanges) {
  			aGroupId.push(sGroupId);
  
  			// clean up existing loaded data for the given group ID
  			this._resetData(sGroupId);
  
  			var aGroupIdRange = mGroupIdRanges[sGroupId];
  
  			for (var i = 0; i < aGroupIdRange.length; i++) {
  				var oGroupIdRange = aGroupIdRange[i];
  				// force reload of every requested index range for the given group ID
  				this._getContextsForParentGroupId(sGroupId, oGroupIdRange.startIndex, oGroupIdRange.length,
  						oGroupIdRange.threshold);
  			}
  
  			// TODO check current code works, but it would be more natural if this _considerRequestGrouping would be outside of this loop
  			var aRequestId = [];
  			for (var j = -1, sGroupId2; (sGroupId2 = aGroupId[++j]) !== undefined; ) {
  				aRequestId.push(this._getRequestId(AnalyticalBinding._requestType.groupMembersQuery, {groupId: sGroupId2}));
  			}
  			this._considerRequestGrouping(aRequestId);
  		}
  	};
  
  	/**
  	 * Gets analytical metadata for the bound OData entity set.
  	 *
  	 * @function
  	 * @name sap.ui.model.analytics.AnalyticalBinding.prototype.getAnalyticalQueryResult
  	 * @return {sap.ui.model.analytics.odata4analytics.QueryResult} analytical metadata for the bound OData entity set
  	 * @public
  	 */
  	AnalyticalBinding.prototype.getAnalyticalQueryResult = function() {
  		return this.oAnalyticalQueryResult;
  	};
  
  
  	/********************************
  	 *** Private section follows
  	 ********************************/
  
  
  	/**
  	 * Enumeration of request types implemented for the analytical binding.
  	 * Every type <T> is implemented with the two methods prepare<T>Request and process<T>Response, names in proper upper camel case notation.
  	 * @private
  	 */
  	AnalyticalBinding._requestType = {
  			groupMembersQuery : 1, // members of a named group G identified by its path /G1/G2/G3/.../G/
  			totalSizeQuery : 2, // total number of entities in result matching all specified filter conditions
  			groupMembersAutoExpansionQuery : 3, // all members residing in a group or sub group w.r.t. a given group ID
  			levelMembersQuery : 4, // members of a given level
  			reloadMeasuresQuery : 5 // measures of a certain entry
  			};
  
  	AnalyticalBinding._artificialRootContextGroupId = "artificialRootContext";
  
  	/**
  	 * Iterates over the given array of hierarchy level filters. For each level filter removes an
  	 * already existing entry from given filter expression and adds a new entry to the filter
  	 * expression.
  	 *
  	 * @param {object[]} aFilters
  	 *   An array of hierarchy level filter objects. Each object has a <code>propertyName</code>
  	 *   property of type string and a <code>level</code> property of type number.
  	 * @param {sap.ui.model.analytics.odata4analytics.FilterExpression} oFilterExpression
  	 *   The FilterExpression to which to add the hierarchy level filters
  	 * @private
  	 */
  	AnalyticalBinding._addHierarchyLevelFilters = function (aFilters, oFilterExpression) {
  		// add level restrictions, if hierarchy is included in request
  		aFilters.forEach(function (oFilter) {
  			oFilterExpression.removeConditions(oFilter.propertyName);
  			oFilterExpression.addCondition(oFilter.propertyName, FilterOperator.EQ, oFilter.level);
  		});
  	};
  
  	/**
  	 * @private
  	 */
  	AnalyticalBinding.prototype._getContextsForParentContext = function(oParentContext, iStartIndex, iLength,
  			iThreshold, iLevel, iNumberOfExpandedLevels, bSupressRequest) {
  
  		if (oParentContext === undefined) {
  			return []; // API robustness
  		}
  		if (oParentContext && oParentContext.getPath() == "/" + AnalyticalBinding._artificialRootContextGroupId) {
  			// special case for artificial root contexts: adjust context to point to the real path
  			oParentContext = this.getModel().getContext("/");
  		}
  		var sParentGroupId = this._getGroupIdFromContext(oParentContext, iLevel);
  		return this._getContextsForParentGroupId(sParentGroupId, iStartIndex, iLength, iThreshold, iNumberOfExpandedLevels, bSupressRequest);
  	};
  
  	/**
  	 * @private
  	 */
  	AnalyticalBinding.prototype._getContextsForParentGroupId = function(sParentGroupId, iStartIndex, iLength,
  			iThreshold, iNumberOfExpandedLevels, bSupressRequest) {
  		if (sParentGroupId === undefined) {
  			return []; // API robustness
  		}
  
  		//	Set default values if start index, threshold, length or number of expanded levels are not defined
  		if (!iStartIndex) {
  			iStartIndex = 0;
  		}
  
  		if (!iLength) {
  			iLength = this.oModel.iSizeLimit;
  		}
  
  		if (this.mFinalLength[sParentGroupId] && this.mLength[sParentGroupId] < iStartIndex + iLength) {
  			iLength = this.mLength[sParentGroupId] - iStartIndex;
  			if (iLength < 0) {
  				oLogger.fatal("invalid start index greater than total group length passed");
  			}
  		}
  
  		if (!iThreshold) {
  			iThreshold = 0;
  		}
  
  		if (!iNumberOfExpandedLevels) {
  			iNumberOfExpandedLevels = 0;
  		}
  		if (sParentGroupId == null) {
  			if (iNumberOfExpandedLevels > 0) {
  				oLogger.fatal("invalid request to determine nodes of root context");
  				return null;
  			}
  		} else {
  			if (this._getGroupIdLevel(sParentGroupId) >= this.aAggregationLevel.length && iNumberOfExpandedLevels > 0) {
  				oLogger.fatal("invalid request to determine nodes of context with group ID " + sParentGroupId);
  				return null;
  			}
  			if (this._getGroupIdLevel(sParentGroupId) + iNumberOfExpandedLevels > this.aAggregationLevel.length) {
  				// need to adjust number of levels to expand
  				iNumberOfExpandedLevels = this.aAggregationLevel.length - this._getGroupIdLevel(sParentGroupId) - 1;
  			}
  		}
  
  		var aContext = [], bLoadContexts, oGroupSection, oGroupExpansionFirstMissingMember, missingMemberCount;
  		var iAggregationLevel = sParentGroupId == null ? 0 : this._getGroupIdLevel(sParentGroupId) + 1;
  		if (!this.aMultiUnitLoadFactor[iAggregationLevel]) {
  			this.aMultiUnitLoadFactor[iAggregationLevel] = 1;
  		}
  
  		var bGroupLevelAutoExpansionIsActive = iNumberOfExpandedLevels > 0 && sParentGroupId != null;
  		if (bGroupLevelAutoExpansionIsActive) {
  			var iMinRequiredLevel = this._getGroupIdLevel(sParentGroupId);
  			var iAutoExpandGroupsToLevel = iMinRequiredLevel + iNumberOfExpandedLevels;
  			var bDataAvailable = true;
  			if (!bSupressRequest) {
  				oGroupExpansionFirstMissingMember = this._calculateRequiredGroupExpansion(sParentGroupId, iAutoExpandGroupsToLevel, iStartIndex, iLength + iThreshold);
  				bDataAvailable = oGroupExpansionFirstMissingMember.groupId_Missing == null;
  				// the following line further reliefs the condition to load data by just looking at the sub-tree
  				bDataAvailable = bDataAvailable
  					// first missing member is in a different upper level sub-tree, e.g. sParentGroupId: /A/B/C groupId_Missing: /A/X
  					|| oGroupExpansionFirstMissingMember.groupId_Missing.length < sParentGroupId.length
  					// first missing member is in a different lower level sub-tree, e.g. sParentGroupId: /A/B groupId_Missing: /A/C/D
  					|| oGroupExpansionFirstMissingMember.groupId_Missing.substring(0, sParentGroupId.length) != sParentGroupId;
  			}
  			if (bDataAvailable) {
  				aContext = this._getLoadedContextsForGroup(sParentGroupId, iStartIndex, iLength);
  			} else {
  				missingMemberCount = iLength + iThreshold;
  			}
  			bLoadContexts = !bDataAvailable;
  			// finally adjust the number of entities to be loaded by the load factor (after(!) all calculations have been made)
  			missingMemberCount = Math.ceil(missingMemberCount * this.aMultiUnitLoadFactor[iAggregationLevel]);
  		} else { // no automatic expansion of group levels
  			aContext = this._getLoadedContextsForGroup(sParentGroupId, iStartIndex, iLength, bSupressRequest);
  			bLoadContexts = false;
  			if (!bSupressRequest) {
  				oGroupSection = this._calculateRequiredGroupSection(sParentGroupId, iStartIndex, iLength, iThreshold, aContext);
  				var bPreloadContexts = oGroupSection.length > 0 && iLength < oGroupSection.length;
  				bLoadContexts = (aContext.length != iLength
  								 && !(this.mFinalLength[sParentGroupId] && aContext.length >= this.mLength[sParentGroupId] - iStartIndex))
  								|| bPreloadContexts;
  				// finally adjust the number of entities to be loaded by the load factor (after(!) all calculations have been made)
  				oGroupSection.length = Math.ceil(oGroupSection.length * this.aMultiUnitLoadFactor[iAggregationLevel]);
  			}
  		}
  
  		if (!bLoadContexts) {
  			// all data available so no request will be issued that might be related to some group of requests
  			this._cleanupGroupingForCompletedRequest(this._getRequestId(AnalyticalBinding._requestType.groupMembersQuery, {groupId: sParentGroupId}));
  		}
  
  		// check if metadata are already available
  		var bExecuteRequest = false;
  		if (this.oModel.getServiceMetadata()) {
  			// If rows are missing send a request
  			if (bLoadContexts) {
  				var bNeedTotalSize = this.bProvideTotalSize && this.iTotalSize == -1 && !this._isRequestPending(this._getRequestId(AnalyticalBinding._requestType.totalSizeQuery));
  				bExecuteRequest = true;
  				var aMembersRequestId;
  				if (this.bUseBatchRequests) {
  					if (bGroupLevelAutoExpansionIsActive) {
  						aMembersRequestId = this._prepareGroupMembersAutoExpansionRequestIds(sParentGroupId, iNumberOfExpandedLevels);
  						for (var i = -1, sRequestId; (sRequestId = aMembersRequestId[++i]) !== undefined; ) {
  							if (this._isRequestPending(sRequestId)) {
  								bExecuteRequest = false;
  								break;
  							}
  						}
  						if (bExecuteRequest) {
  							this.aBatchRequestQueue.push([ AnalyticalBinding._requestType.groupMembersAutoExpansionQuery, sParentGroupId, oGroupExpansionFirstMissingMember, missingMemberCount, iNumberOfExpandedLevels ]);
  						}
  					} else { // ! bGroupLevelAutoExpansionIsActive
  						bExecuteRequest = !this._isRequestPending(this._getRequestId(AnalyticalBinding._requestType.groupMembersQuery, {groupId: sParentGroupId}));
  						if (bExecuteRequest) {
  							this.aBatchRequestQueue.push([ AnalyticalBinding._requestType.groupMembersQuery, sParentGroupId, oGroupSection.startIndex, oGroupSection.length ]);
  							aMembersRequestId = [ this._getRequestId(AnalyticalBinding._requestType.groupMembersQuery, {groupId: sParentGroupId}) ];
  						}
  					}
  					if (bExecuteRequest && bNeedTotalSize) {
  						aMembersRequestId.push(this._getRequestId(AnalyticalBinding._requestType.totalSizeQuery));
  						this._considerRequestGrouping(aMembersRequestId);
  						this.aBatchRequestQueue.push([ AnalyticalBinding._requestType.totalSizeQuery ]);
  					}
  					if (bExecuteRequest) {
  						if (sParentGroupId == null) { // root node is requested, so discard all not received responses, because the entire table must be set up from scratch
  							this._abortAllPendingRequests();
  						}
  						Promise.resolve().then(AnalyticalBinding.prototype._processRequestQueue.bind(this));
  					}
  				} else { // ! bUseBatchRequests
  					var oMemberRequestDetails;
  					if (bGroupLevelAutoExpansionIsActive) {
  						aMembersRequestId = this._prepareGroupMembersAutoExpansionRequestIds(sParentGroupId, iNumberOfExpandedLevels);
  						for (var j = -1, sMemberRequestId; (sMemberRequestId = aMembersRequestId[++j]) !== undefined; ) {
  							if (this._isRequestPending(sMemberRequestId)) {
  								bExecuteRequest = false;
  								break;
  							}
  						}
  						if (bExecuteRequest) {
  							oMemberRequestDetails = this._prepareGroupMembersAutoExpansionQueryRequest(AnalyticalBinding._requestType.groupMembersAutoExpansionQuery, sParentGroupId, oGroupExpansionFirstMissingMember, missingMemberCount, iNumberOfExpandedLevels);
  						}
  					} else { // ! bGroupLevelAutoExpansionIsActive
  						bExecuteRequest = !this._isRequestPending(this._getRequestId(AnalyticalBinding._requestType.groupMembersQuery, {groupId: sParentGroupId}));
  						if (bExecuteRequest) {
  							oMemberRequestDetails = this._prepareGroupMembersQueryRequest(AnalyticalBinding._requestType.groupMembersQuery, sParentGroupId, oGroupSection.startIndex, oGroupSection.length);
  							aMembersRequestId = [ oMemberRequestDetails.sRequestId ];
  						}
  					}
  					if (bExecuteRequest) {
  						if (sParentGroupId == null) { // root node is requested, so discard all not received responses, because the entire table must be set up from scratch
  							this._abortAllPendingRequests();
  						}
  
  						this._executeQueryRequest(oMemberRequestDetails);
  						if (bNeedTotalSize && !oMemberRequestDetails.bIsFlatListRequest) {
  							aMembersRequestId.push(this._getRequestId(AnalyticalBinding._requestType.totalSizeQuery));
  							this._considerRequestGrouping(aMembersRequestId);
  							this._executeQueryRequest(this._prepareTotalSizeQueryRequest(AnalyticalBinding._requestType.totalSizeQuery));
  						}
  					}
  				}
  			}
  		}
  		return aContext;
  	};
  
  
  	/**
  	 * Computes the hierarchy level filters for all entries in <code>mHierarchyDetailsByName</code>
  	 * and adds for each entry a recursive hierarchy to the given analytical query request.
  	 * If the given group ID is null nothing is done and if the given group ID is not "/" an error
  	 * is logged and an empty array is returned.
  	 *
  	 * @param {sap.ui.model.analytics.odata4analytics.QueryResultRequest} oAnalyticalQueryRequest
  	 *   The analytical query request to which to add the recursive hierarchy
  	 * @param {string} sGroupId
  	 *   The group ID; has to be "/" or null otherwise an error is logged and an empty array is
  	 *   returned
  	 * @returns {object[]} An array of hierarchy level filters. Each filter has a
  	 *   <code>propertyName</code> property of type string and a <code>level</code> property of type
  	 *   number.
  	 * @private
  	 */
  	AnalyticalBinding.prototype._getHierarchyLevelFiltersAndAddRecursiveHierarchy
  			= function (oAnalyticalQueryRequest, sGroupId) {
  		var aHierarchyKeys,
  			aHierarchyLevelFilters = [],
  			that = this;
  
  		if (sGroupId === null) {
  			return aHierarchyLevelFilters;
  		}
  
  		aHierarchyKeys = Object.keys(this.mHierarchyDetailsByName);
  		if (aHierarchyKeys.length > 0 && sGroupId !== "/") {
  			oLogger.error("Hierarchy cannot be requested for members of a group",
  				sGroupId);
  			return aHierarchyLevelFilters;
  		}
  
  		aHierarchyKeys.forEach(function (sHierarchyKey) {
  			var oHierarchyDetails = that.mHierarchyDetailsByName[sHierarchyKey];
  
  			oAnalyticalQueryRequest.addRecursiveHierarchy(oHierarchyDetails.dimensionName,
  				!!oHierarchyDetails.nodeExternalKeyName,
  				!!oHierarchyDetails.nodeTextName);
  			aHierarchyLevelFilters.push({
  				propertyName : oHierarchyDetails.nodeLevelName,
  				level : oHierarchyDetails.level
  			});
  		});
  		return aHierarchyLevelFilters;
  	};
  
  	/**
  	 * Filters out hierarchy dimensions from given aggregation level.
  	 *
  	 * @param {string[]} aAggregationLevel
  	 *   Array of dimension property names which define the aggregation level
  	 * @returns {string[]} Array of non hierarchy dimensions
  	 * @private
  	 */
  	AnalyticalBinding.prototype._getNonHierarchyDimensions = function (aAggregationLevel) {
  		var that = this;
  
  		return aAggregationLevel.filter(function (sDimension) {
  			return !that.oDimensionDetailsSet[sDimension].isHierarchyDimension;
  		});
  	};
  
  	AnalyticalBinding.prototype._processRequestQueue = function(aRequestQueue) {
  		// if no argument is given: use the shared member aBatchRequestQueue
  		if (aRequestQueue === undefined || aRequestQueue === null) {
  			//safety check: empty array fallback in case the "global" batch queue is not defined yet
  			aRequestQueue = this.aBatchRequestQueue || [];
  		}
  		// step out if the request queue is still empty after our previous checks
  		if (aRequestQueue.length == 0) {
  			return;
  		}
  
  		var aRequestDetails = [];
  		var bFoundFlatListRequest = false;
  		var i, oRequestDetails, aRequestQueueEntry;
  
  		// create request objects: process group member requests first to detect flat list requests
  		for (i = -1; (aRequestQueueEntry = aRequestQueue[++i]) !== undefined;) {
  			if (aRequestQueueEntry[0] == AnalyticalBinding._requestType.groupMembersQuery) { // request type is at array index 0
  				oRequestDetails = AnalyticalBinding.prototype._prepareGroupMembersQueryRequest.apply(this, aRequestQueueEntry);
  				bFoundFlatListRequest = bFoundFlatListRequest || oRequestDetails.bIsFlatListRequest;
  				aRequestDetails.push(oRequestDetails);
  			}
  		}
  
  		// create request objects for all other request types
  		for (i = -1; (aRequestQueueEntry = aRequestQueue[++i]) !== undefined;) {
  			oRequestDetails = null;
  			switch (aRequestQueueEntry[0]) { // different request types
  			case AnalyticalBinding._requestType.groupMembersQuery:
  				continue; // handled above
  			case AnalyticalBinding._requestType.totalSizeQuery:
  				if (!bFoundFlatListRequest) {
  					oRequestDetails = AnalyticalBinding.prototype._prepareTotalSizeQueryRequest.apply(this, aRequestQueueEntry);
  					aRequestDetails.push(oRequestDetails);
  				}
  				break;
  			case AnalyticalBinding._requestType.groupMembersAutoExpansionQuery:
  				oRequestDetails = AnalyticalBinding.prototype._prepareGroupMembersAutoExpansionQueryRequest.apply(this, aRequestQueueEntry);
  				for (var j = -1, oLevelMembersRequestDetails; (oLevelMembersRequestDetails = oRequestDetails.aGroupMembersAutoExpansionRequestDetails[++j]) !== undefined; ) {
  					aRequestDetails.push(oLevelMembersRequestDetails);
  				}
  				break;
  			case AnalyticalBinding._requestType.reloadMeasuresQuery: {
  				var aReloadMeasureRequestDetails = aRequestQueueEntry[1];
  				for (var k = -1, oReloadMeasureRequestDetails; (oReloadMeasureRequestDetails = aReloadMeasureRequestDetails[++k]) !== undefined; ) {
  					aRequestDetails.push(oReloadMeasureRequestDetails);
  				}
  				break;
  			}
  			default:
  				oLogger.fatal("unhandled request type " + aRequestQueue[i][0]);
  				continue;
  			}
  		}
  
  		// execute them either directly in case of a single request or via a batch request
  		if (aRequestDetails.length > 1) {
  			this._executeBatchRequest(aRequestDetails);
  		} else {
  			this._executeQueryRequest(aRequestDetails[0]);
  		}
  
  		// clean up request queue after processing, if it is based on the shared member request queue
  		if (aRequestQueue === this.aBatchRequestQueue) {
  			this.aBatchRequestQueue = [];
  		}
  	};
  
  	/** *************************************************************** */
  	/**
  	 * @private
  	 */
  	AnalyticalBinding.prototype._prepareGroupMembersQueryRequest = function(iRequestType, sGroupId, iStartIndex, iLength) {
  		var aGroupId = [],
  			// array of sap.ui.model.Sorter like objects ({sPatch, bDescending})
  			aGroupingSorters = [],
  			aHierarchyLevelFilters;
  
  		// (0) set up analytical OData request object
  		var oAnalyticalQueryRequest = new odata4analytics.QueryResultRequest(this.oAnalyticalQueryResult);
  		oAnalyticalQueryRequest.setResourcePath(this._getResourcePath());
  		oAnalyticalQueryRequest.getSortExpression().clear();
  
  		// (1) analyze aggregation level of sGroupId
  
  		// indexes to elements of this.aMaxAggregationLevel marking begin and end of the requested child level
  		var iChildGroupFromLevel = 0, iChildGroupToLevel = -1;
  		if (sGroupId) {
  			aGroupId = this._getGroupIdComponents(sGroupId);
  			iChildGroupFromLevel = iChildGroupToLevel = aGroupId.length;
  
  			var iUngroupedParentLevelCount = 0;
  			// determine offset for child level (depends on grouped column property of higher aggregation levels)
  			// Ex: Assume aMaxAggregationLevel with (G=grouped,U=ungrouped): [ G1 U1 U2 G2 U3 U4 G3 F5 F6 ... ]
  			// For sGroupId = "G1/G2", initial iChildGroupFromLevel is 2. The following loop will increment it to 4
  			// and consequently point to U3
  			for (var j = 0, iLevel = 0; j < iChildGroupFromLevel; iLevel++) {
  				if (this.oDimensionDetailsSet[this.aMaxAggregationLevel[iLevel]].grouped == false) {
  					++iUngroupedParentLevelCount;
  				} else {
  					++j;
  				}
  			}
  			// adjust child levels by number of ungrouped parent levels!
  			iChildGroupFromLevel = iChildGroupToLevel = iChildGroupFromLevel + iUngroupedParentLevelCount;
  
  			// determine index range for aggregation levels included in child level
  			// (rule: take all lower levels up to and including the first grouped level; G3 in above example
  			if (this.aMaxAggregationLevel.length > 0) {
  				while (this.oDimensionDetailsSet[this.aMaxAggregationLevel[iChildGroupToLevel]].grouped == false) {
  					if (++iChildGroupToLevel == this.aMaxAggregationLevel.length) {
  						break;
  					}
  				}
  			}
  		}
  
  		// (2) determine if the sub groups will effectively represent leafs (relevant for un-"total"ed columns, see below)
  		var bIsLeafGroupsRequest = iChildGroupToLevel >= this.aMaxAggregationLevel.length - 1;
  
  		// (3) set aggregation level for child nodes
  		// need to distinguish between regular dimensions and hierarchy dimensions
  		aHierarchyLevelFilters
  			= this._getHierarchyLevelFiltersAndAddRecursiveHierarchy(oAnalyticalQueryRequest,
  				sGroupId);
  
  		var aAggregationLevel = this.aMaxAggregationLevel.slice(0, iChildGroupToLevel + 1);
  		var aAggregationLevelNoHierarchy = this._getNonHierarchyDimensions(aAggregationLevel);
  		oAnalyticalQueryRequest.setAggregationLevel(aAggregationLevelNoHierarchy);
  		for (var i = 0; i < aAggregationLevelNoHierarchy.length; i++) {
  			// specify components requested for this level (key, text, attributes)
  			var oDimensionDetails = this.oDimensionDetailsSet[aAggregationLevelNoHierarchy[i]];
  			// as we combine the key and text in the group header we also need the text!
  			var bIncludeText = (oDimensionDetails.textPropertyName != undefined);
  			oAnalyticalQueryRequest.includeDimensionKeyTextAttributes(oDimensionDetails.name, // bIncludeKey: No, always needed!
  			true, bIncludeText, oDimensionDetails.aAttributeName);
  
  			// define a default sort order in case no sort criteria have been provided externally
  			if (oDimensionDetails.grouped) {
  				aGroupingSorters.push({
  					sPath : aAggregationLevelNoHierarchy[i],
  					bDescending : false
  				});
  			}
  		}
  
  		// (4) set filter
  		var oFilterExpression = oAnalyticalQueryRequest.getFilterExpression();
  		oFilterExpression.clear();
  		if (this.aApplicationFilter) {
  			oFilterExpression.addUI5FilterConditions(this.aApplicationFilter);
  		}
  		if (this.aControlFilter) {
  			oFilterExpression.addUI5FilterConditions(this.aControlFilter);
  		}
  
  		if (iChildGroupFromLevel >= 1) {
  			for (var k = 0, l = aGroupId.length; k < l; k++) {
  				oFilterExpression.removeConditions(this.aAggregationLevel[k]);
  				oFilterExpression.addCondition(this.aAggregationLevel[k], FilterOperator.EQ, aGroupId[k]);
  			}
  		}
  		AnalyticalBinding._addHierarchyLevelFilters(aHierarchyLevelFilters, oFilterExpression);
  
  		// (5) set measures as requested per column
  		var bIncludeRawValue;
  		var bIncludeFormattedValue;
  		var bIncludeUnitProperty;
  		var oMeasureDetails;
  
  		var aSelectedUnitPropertyName = [];
  
  		if (sGroupId != null || this.bProvideGrandTotals
  				// get also grand total for group ID "null" (virtual root), independent of
  				// bProvideGrandTotals, if there are sorters in this.aSorter and they need to be
  				// applied to groups
  				|| (this._canApplySortersToGroups() && this.aSorter.length > 0)) {
  			// select measures if the requested group is not the root context i.e. the grand totals row, or grand totals shall be determined
  			oAnalyticalQueryRequest.setMeasures(this.aMeasureName);
  
  			for ( var sMeasureName in this.oMeasureDetailsSet) {
  				oMeasureDetails = this.oMeasureDetailsSet[sMeasureName];
  				if (!bIsLeafGroupsRequest && this.mAnalyticalInfoByProperty[sMeasureName].total == false) {
  					bIncludeRawValue = false;
  					bIncludeFormattedValue = false;
  					bIncludeUnitProperty = false;
  				} else {
  					bIncludeRawValue = (oMeasureDetails.rawValuePropertyName != undefined);
  					bIncludeFormattedValue = (oMeasureDetails.formattedValuePropertyName != undefined);
  					bIncludeUnitProperty = (oMeasureDetails.unitPropertyName != undefined);
  					if (bIncludeUnitProperty) {
  						// remember unit property together with using measure raw value property for response analysis in success handler
  						if (aSelectedUnitPropertyName.indexOf(oMeasureDetails.unitPropertyName) == -1) {
  							aSelectedUnitPropertyName.push(oMeasureDetails.unitPropertyName);
  						}
  					}
  				}
  				oAnalyticalQueryRequest.includeMeasureRawFormattedValueUnit(oMeasureDetails.name, bIncludeRawValue,
  						bIncludeFormattedValue, bIncludeUnitProperty);
  			}
  			// exclude those unit properties from the selected that are included in the current aggregation level
  			for (var n in aAggregationLevelNoHierarchy) {
  				var iMatchingIndex;
  				if ((iMatchingIndex = aSelectedUnitPropertyName.indexOf(aAggregationLevelNoHierarchy[n])) != -1) {
  					aSelectedUnitPropertyName.splice(iMatchingIndex, 1);
  				}
  			}
  		}
  
  		// (6) set sort order
  		this._addSorters(oAnalyticalQueryRequest.getSortExpression(), aGroupingSorters);
  
  		// (7) set result page boundaries
  		if (iLength == 0) {
  			oLogger.fatal("unhandled case: load 0 entities of sub group");
  		}
  		var oKeyIndexMapping = this._getKeyIndexMapping(sGroupId, iStartIndex);
  		if (!this.bNoPaging) {
  			oAnalyticalQueryRequest.setResultPageBoundaries(oKeyIndexMapping.iServiceKeyIndex + 1, oKeyIndexMapping.iServiceKeyIndex + iLength);
  		}
  
  		// (8) request result entity count
  		oAnalyticalQueryRequest.setRequestOptions(null, !this.mFinalLength[sGroupId]);
  
  		return {
  			iRequestType : iRequestType,
  			sRequestId : this._getRequestId(AnalyticalBinding._requestType.groupMembersQuery, {groupId: sGroupId}),
  			oAnalyticalQueryRequest : oAnalyticalQueryRequest,
  			sGroupId : sGroupId,
  			aSelectedUnitPropertyName : aSelectedUnitPropertyName,
  			aAggregationLevel : aAggregationLevel,
  			bIsFlatListRequest : bIsLeafGroupsRequest && iChildGroupFromLevel == 0,
  			bIsLeafGroupsRequest : bIsLeafGroupsRequest,
  			iStartIndex : iStartIndex,
  			iLength : iLength,
  			oKeyIndexMapping : oKeyIndexMapping
  		};
  	};
  
  	/**
  	 * @private
  	 */
  	AnalyticalBinding.prototype._prepareTotalSizeQueryRequest = function(iRequestType) {
  		var aHierarchyLevelFilters;
  
  		// (0) set up analytical OData request object
  		var oAnalyticalQueryRequest = new odata4analytics.QueryResultRequest(this.oAnalyticalQueryResult);
  		oAnalyticalQueryRequest.setResourcePath(this._getResourcePath());
  
  		// (1) set aggregation level
  		// need to distinguish between regular dimensions and hierarchy dimensions
  		aHierarchyLevelFilters
  			= this._getHierarchyLevelFiltersAndAddRecursiveHierarchy(oAnalyticalQueryRequest, "/");
  		oAnalyticalQueryRequest
  			.setAggregationLevel(this._getNonHierarchyDimensions(this.aMaxAggregationLevel));
  
  		oAnalyticalQueryRequest.setMeasures([]);
  
  		// (2) set filter
  		var oFilterExpression = oAnalyticalQueryRequest.getFilterExpression();
  		oFilterExpression.clear();
  		if (this.aApplicationFilter) {
  			oFilterExpression.addUI5FilterConditions(this.aApplicationFilter);
  		}
  		if (this.aControlFilter) {
  			oFilterExpression.addUI5FilterConditions(this.aControlFilter);
  		}
  		AnalyticalBinding._addHierarchyLevelFilters(aHierarchyLevelFilters, oFilterExpression);
  
  		// (2) fetch no data
  		oAnalyticalQueryRequest.setRequestOptions(null, null, true);
  
  		// (3) request result entity count
  		oAnalyticalQueryRequest.setRequestOptions(null, true);
  
  		return {
  			iRequestType : iRequestType,
  			sRequestId : this._getRequestId(AnalyticalBinding._requestType.totalSizeQuery),
  			oAnalyticalQueryRequest : oAnalyticalQueryRequest
  		};
  	};
  
  
  	/**
  	 * @private
  	 */
  	AnalyticalBinding.prototype._prepareGroupMembersAutoExpansionQueryRequest = function(iRequestType, sGroupId, oGroupExpansionFirstMissingMember, iLength, iNumberOfExpandedLevels) {
  		var that = this;
  
  		// local helper function for creating filter expressions for all group level requests
  		/*
  		 * Let G be the group ID for the root for the first missing member, e.g. G = /A/B/C/D/.../W/X/, and (optional) startIndex_Missing > 0 for the children of X.
  		 * Let P_1, P_2, ... be the properties for the different grouping levels.
  		 * Then, for every level l, 1 <= l <= iAutoExpandGroupsToLevel, the filter expression
  		 * is // every such expression is an instance of aLevelFilterCondition, see in code below
  		 *   [0] ( P_1 = A and P_2 = B and .. P_l >= X )
  		 *   [1] or ( P_1 = A and P_2 = B and .. P_(l-1) > W )    // every such line is an instance of aIntermediateLevelFilterCondition, see in code below
  		 *       ...
  		 *   [N] or ( P_1 > A )
  		 *   assuming that P_1, P_2, ... are all to be sorted in ascending order. For any deviation, replace > with <.
  		 *
  		 *   Additional rules considered:
  		 *   (R1) For every auto-expand level with a higher number than the level of the first missing member,
  		 *     the strict comparison (< or >) has to include equality (<= or >=) to match all needed members of these deep levels.
  		 *   (R2) If startIndex_Missing > 0, then the R1 does not apply. here,
  		 *        (R2.1) the strict comparison (< or >) must be replaced by equality (=)
  		 *        (R2.2) the partial filter expression for every auto-expand level with a higher number than the level of the first missing member
  		 *               must be extended by a condition P_Y > Y, where Y is the child of X at position startIndex_Missing - 1,
  		 *               and P_Y is the property for this grouping level.
  		 */
  		var prepareLevelFilterExpressions = function(oGroupExpansionFirstMissingMember, iAutoExpandGroupsToLevel) {
  			var aFilterArray = [];
  
  			if (oGroupExpansionFirstMissingMember.groupId_Missing == null) {
  				oLogger.fatal("missing group Id not present");
  				return aFilterArray;
  			}
  			var aGroupIdComponents_Missing = that._getGroupIdComponents(oGroupExpansionFirstMissingMember.groupId_Missing);
  			var iGroupIdLevel_Missing = aGroupIdComponents_Missing.length;
  			if (iGroupIdLevel_Missing > iAutoExpandGroupsToLevel) {
  				oLogger.fatal("the given group ID is too deep for requested level for auto expansion");
  				return aFilterArray;
  			}
  
  			// create template for every term of the filter expression
  			var aTemplateFilter = [];
  			for (var i = 0; i < iGroupIdLevel_Missing; i++) {
  				var sGroupProperty = that.aAggregationLevel[i];
  				var sValue = aGroupIdComponents_Missing[i];
  				var sFilterOperator = that._getFilterOperatorMatchingPropertySortOrder(sGroupProperty);
  				aTemplateFilter[i] = new Filter(sGroupProperty, sFilterOperator, sValue);
  			}
  
  			// if first missing member start within a partially loaded group, an extra condition will be needed below
  			var oFirstMissingMemberStartIndexLastKnownFilterCondition = null;
  			if (oGroupExpansionFirstMissingMember.startIndex_Missing > 0) {
  				var oFirstMissingMemberStartIndexLastKnownGroupContextKey = that._getKey(oGroupExpansionFirstMissingMember.groupId_Missing,
  						oGroupExpansionFirstMissingMember.startIndex_Missing - 1);
  				var oFirstMissingMemberStartIndexLastKnownObject = that.oModel.getObject("/" + oFirstMissingMemberStartIndexLastKnownGroupContextKey);
  				var sFirstMissingMemberStartIndexAggregationLevel = that.aAggregationLevel[iGroupIdLevel_Missing];
  				var sFirstMissingMemberStartIndexLastKnownValue = oFirstMissingMemberStartIndexLastKnownObject
  					[sFirstMissingMemberStartIndexAggregationLevel];
  				oFirstMissingMemberStartIndexLastKnownFilterCondition = new Filter(sFirstMissingMemberStartIndexAggregationLevel,
  						that._getFilterOperatorMatchingPropertySortOrder(sFirstMissingMemberStartIndexAggregationLevel, false),
  						sFirstMissingMemberStartIndexLastKnownValue);
  			}
  
  
  			// now create the filter expressions (filter object arrays) for every group level to be requested
  			for (var iLevel = 0; iLevel < iAutoExpandGroupsToLevel; iLevel++) {
  				var aLevelFilterCondition = [];
  				var iNumberOfIntermediateLevelConditions = Math.min(iGroupIdLevel_Missing, iLevel + 1);
  				for (var iIntermediateLevel = 0; iIntermediateLevel < iNumberOfIntermediateLevelConditions; iIntermediateLevel++) {
  					var aIntermediateLevelFilterCondition = [];
  					var iNumberOfLevelConditions = Math.min(iGroupIdLevel_Missing, iIntermediateLevel + 1);
  					var bAddExtraConditionForFirstMissingMemberStartIndexLastKnown =
  						oGroupExpansionFirstMissingMember.startIndex_Missing > 0;
  					for (var iLevelCondition = 0; iLevelCondition < iNumberOfLevelConditions; iLevelCondition++) {
  						// create filter condition from template
  						var oFilterCondition = new Filter("x", FilterOperator.EQ, "x");
  						oFilterCondition = jQuery.extend(true, oFilterCondition, aTemplateFilter[iLevelCondition]);
  
  						if (iNumberOfLevelConditions > 1 && iLevelCondition < iNumberOfLevelConditions - 1) {
  							oFilterCondition.sOperator = FilterOperator.EQ;
  						}
  						if (iLevelCondition == iGroupIdLevel_Missing - 1
  							&& iLevel > iGroupIdLevel_Missing - 1
  							&& !bAddExtraConditionForFirstMissingMemberStartIndexLastKnown) { // rule (R1)
  							if (oFilterCondition.sOperator == FilterOperator.GT) {
  								oFilterCondition.sOperator = FilterOperator.GE;
  							} else { // it must be LT
  								oFilterCondition.sOperator = FilterOperator.LE;
  							}
  						}
  						aIntermediateLevelFilterCondition.push(oFilterCondition);
  					}
  					// create the instance for ( P_1 = A and P_2 = B and .. P_(l-1) > W )
  					if (aIntermediateLevelFilterCondition.length > 0) {
  						aLevelFilterCondition.push(new Filter(aIntermediateLevelFilterCondition, true));
  						// add an extra intermediate filter condition to reflect start position at oGroupExpansionFirstMissingMember.startIndex_Missing
  						if (iLevel > iGroupIdLevel_Missing - 1
  							&& iIntermediateLevel == iGroupIdLevel_Missing - 1
  							&& bAddExtraConditionForFirstMissingMemberStartIndexLastKnown) { // rule (R2)
  							// create a copy of the constructed intermediate filter condition
  							var aStartIndexFilterCondition = [];
  							for (var j = 0; j < aIntermediateLevelFilterCondition.length; j++) {
  								var oConditionCopy = new Filter("x", FilterOperator.EQ, "x");
  								oConditionCopy = jQuery.extend(true, oConditionCopy, aIntermediateLevelFilterCondition[j]);
  								aStartIndexFilterCondition.push(oConditionCopy);
  							}
  							aStartIndexFilterCondition[iGroupIdLevel_Missing - 1].sOperator = FilterOperator.EQ; // (R2.1)
  							aStartIndexFilterCondition.push(oFirstMissingMemberStartIndexLastKnownFilterCondition); // (R2.2)
  
  							aLevelFilterCondition.push(new Filter(aStartIndexFilterCondition, true));
  							break;
  						}
  					}
  				}
  				// create the entire filter expression
  				if (aLevelFilterCondition.length > 0) {
  					aFilterArray[iLevel] = new Filter(aLevelFilterCondition, false);
  				} else {
  					aFilterArray[iLevel] = null;
  				}
  			}
  
  			return aFilterArray;
  		};
  
  		// local helper function for requesting members of a given level (across groups) - copied from _prepareGroupMembersQueryRequest & adapted
  		var prepareLevelMembersQueryRequest = function(iRequestType, sGroupId, iLevel, oGroupContextFilter,
  				iStartIndex, iLength, bAvoidLengthUpdate, bUseStartIndexForSkip) {
  			var aHierarchyLevelFilters;
  
  			// (1) set up analytical OData request object
  			var oAnalyticalQueryRequest = new odata4analytics.QueryResultRequest(that.oAnalyticalQueryResult);
  			oAnalyticalQueryRequest.setResourcePath(that._getResourcePath());
  			oAnalyticalQueryRequest.getSortExpression().clear();
  			// (2) analyze aggregation level of sGroupId
  
  			// indexes to elements of this.aMaxAggregationLevel marking begin and end of the requested child level
  			var iChildGroupFromLevel = 0, iChildGroupToLevel = -1;
  			iChildGroupFromLevel = iChildGroupToLevel = iLevel - 1;
  
  			var iUngroupedParentLevelCount = 0;
  			// determine offset for child level (depends on grouped column property of higher aggregation levels)
  			// Ex: Assume aMaxAggregationLevel with (G=grouped,U=ungrouped): [ G1 U1 U2 G2 U3 U4 G3 F5 F6 ... ]
  			// For sGroupId = "G1/G2", initial iChildGroupFromLevel is 2. The following loop will increment it to 4
  			// and consequently point to U3
  			for (var i = 0, iParentLevel = 0; i < iChildGroupFromLevel; iParentLevel++) {
  				if (that.oDimensionDetailsSet[that.aMaxAggregationLevel[iParentLevel]].grouped == false) {
  					++iUngroupedParentLevelCount;
  				} else {
  					++i;
  				}
  			}
  			// adjust child levels by number of ungrouped parent levels!
  			iChildGroupFromLevel = iChildGroupToLevel = iChildGroupFromLevel + iUngroupedParentLevelCount;
  
  			// determine index range for aggregation levels included in child level
  			// (rule: take all lower levels up to and including the first grouped level; G3 in above example
  			if (that.aMaxAggregationLevel.length > 0) {
  				while (that.oDimensionDetailsSet[that.aMaxAggregationLevel[iChildGroupToLevel]].grouped == false) {
  					if (++iChildGroupToLevel == that.aMaxAggregationLevel.length) {
  						break;
  					}
  				}
  			}
  
  			// determine if the sub groups will effectively represent leafs (relevant for un-"total"ed columns, see below)
  			var bIsLeafGroupsRequest = iChildGroupToLevel >= that.aMaxAggregationLevel.length - 1;
  
  			// (3) set aggregation level for child nodes
  			aHierarchyLevelFilters
  				= that._getHierarchyLevelFiltersAndAddRecursiveHierarchy(oAnalyticalQueryRequest,
  					sGroupId);
  
  			var aAggregationLevel = that.aMaxAggregationLevel.slice(0, iChildGroupToLevel + 1);
  			oAnalyticalQueryRequest.setAggregationLevel(aAggregationLevel);
  
  			for (var l = 0; l < aAggregationLevel.length; l++) {
  				var oDimensionDetails = that.oDimensionDetailsSet[aAggregationLevel[l]];
  				var bIncludeText = (oDimensionDetails.textPropertyName != undefined);
  				oAnalyticalQueryRequest.includeDimensionKeyTextAttributes(oDimensionDetails.name, // bIncludeKey: No, always needed!
  				true, bIncludeText, oDimensionDetails.aAttributeName);
  
  				// define a default sort order in case no sort criteria have been provided externally
  				if (oDimensionDetails.grouped) {
  					oAnalyticalQueryRequest.getSortExpression().addSorter(aAggregationLevel[l], odata4analytics.SortOrder.Ascending);
  				}
  			}
  
  			// (4) set filter
  			var oFilterExpression = oAnalyticalQueryRequest.getFilterExpression();
  			oFilterExpression.clear();
  			if (that.aApplicationFilter) {
  				oFilterExpression.addUI5FilterConditions(that.aApplicationFilter);
  			}
  			if (that.aControlFilter) {
  				oFilterExpression.addUI5FilterConditions(that.aControlFilter);
  			}
  			if (oGroupContextFilter) {
  				oFilterExpression.addUI5FilterConditions([oGroupContextFilter]);
  			}
  			AnalyticalBinding._addHierarchyLevelFilters(aHierarchyLevelFilters, oFilterExpression);
  
  			// (5) set measures as requested per column
  			var bIncludeRawValue;
  			var bIncludeFormattedValue;
  			var bIncludeUnitProperty;
  			var oMeasureDetails;
  
  			var aSelectedUnitPropertyName = [];
  
  			// select measures if the requested group is not the root context i.e. the grand totals row, or grand totals shall be determined
  			oAnalyticalQueryRequest.setMeasures(that.aMeasureName);
  
  			for ( var sMeasureName in that.oMeasureDetailsSet) {
  				oMeasureDetails = that.oMeasureDetailsSet[sMeasureName];
  				if (!bIsLeafGroupsRequest && that.mAnalyticalInfoByProperty[sMeasureName].total == false) {
  					bIncludeRawValue = false;
  					bIncludeFormattedValue = false;
  					bIncludeUnitProperty = false;
  				} else {
  					bIncludeRawValue = (oMeasureDetails.rawValuePropertyName != undefined);
  					bIncludeFormattedValue = (oMeasureDetails.formattedValuePropertyName != undefined);
  					bIncludeUnitProperty = (oMeasureDetails.unitPropertyName != undefined);
  					if (bIncludeUnitProperty) {
  						// remember unit property together with using measure raw value property for response analysis in success handler
  						if (aSelectedUnitPropertyName.indexOf(oMeasureDetails.unitPropertyName) == -1) {
  							aSelectedUnitPropertyName.push(oMeasureDetails.unitPropertyName);
  						}
  					}
  				}
  				oAnalyticalQueryRequest.includeMeasureRawFormattedValueUnit(oMeasureDetails.name, bIncludeRawValue,
  						bIncludeFormattedValue, bIncludeUnitProperty);
  			}
  			// exclude those unit properties from the selected that are included in the current aggregation level
  			for ( var j in aAggregationLevel) {
  				var iMatchingIndex;
  				if ((iMatchingIndex = aSelectedUnitPropertyName.indexOf(aAggregationLevel[j])) != -1) {
  					aSelectedUnitPropertyName.splice(iMatchingIndex, 1);
  				}
  			}
  
  			// (6) set sort order
  			var oSorter = oAnalyticalQueryRequest.getSortExpression();
  			for (var k = 0; k < that.aSorter.length; k++) {
  				if (that.aSorter[k]) {
  					oSorter.addSorter(that.aSorter[k].sPath, that.aSorter[k].bDescending ? odata4analytics.SortOrder.Descending : odata4analytics.SortOrder.Ascending);
  				}
  			}
  
  			// (7) set result page boundaries
  			if (iLength == 0) {
  				oLogger.fatal("unhandled case: load 0 entities of sub group");
  			}
  			var iEffectiveStartIndex = iStartIndex;
  			if (!bUseStartIndexForSkip) {
  				iEffectiveStartIndex = 0; // and the skip-value is encoded in the filter expression; still the start index is relevant and kept (see below) for booking the result entries
  			} else {
  				// when bUseStartIndexForSkip is set and no filter conditions are given, the top level must also be paged
  				// calculate the number of loaded entries per level
  				var iServiceLengthForGroupIdMissing = 0;
  				for (var sGID in that.mServiceKey) {
  					if (sGID.split("/").length === iLevel + 1) {
  						iServiceLengthForGroupIdMissing += that.mServiceKey[sGID].length;
  					}
  				}
  
  				iEffectiveStartIndex = Math.max(iEffectiveStartIndex, iServiceLengthForGroupIdMissing);
  			}
  
  			if (!that.bNoPaging) {
  				oAnalyticalQueryRequest.setResultPageBoundaries(iEffectiveStartIndex + 1, iLength);
  			}
  
  			return {
  				iRequestType : iRequestType,
  				sRequestId : null, // set by caller
  				oAnalyticalQueryRequest : oAnalyticalQueryRequest,
  				iLevel : iLevel,
  				aSelectedUnitPropertyName : aSelectedUnitPropertyName,
  				aAggregationLevel : aAggregationLevel,
  				bIsFlatListRequest : bIsLeafGroupsRequest,
  				bIsLeafGroupsRequest : bIsLeafGroupsRequest,
  				iStartIndex : iStartIndex,
  				iLength : iLength,
  				bAvoidLengthUpdate : bAvoidLengthUpdate
  			};
  		};
  
  		// function implementation starts here
  		var aGroupMembersAutoExpansionRequestDetails = [];
  		var aRequestId = [];
  		if (!oGroupExpansionFirstMissingMember) {
  			oLogger.fatal("no first missing group member specified");
  		}
  		var iAutoExpandGroupsToLevel = this._getGroupIdLevel(sGroupId) + iNumberOfExpandedLevels + 1;
  		var aGroupIdComponents_Missing = that._getGroupIdComponents(oGroupExpansionFirstMissingMember.groupId_Missing);
  		var iGroupIdLevel_Missing = aGroupIdComponents_Missing.length;
  		var aFilterArray = prepareLevelFilterExpressions(oGroupExpansionFirstMissingMember, iAutoExpandGroupsToLevel);
  		var sGroupIdAtLevel;
  
  		for (var iLevel = 1; iLevel <= iAutoExpandGroupsToLevel; iLevel++) {
  			var iStartIndex;
  			// determine start index
  			if (iLevel >= iGroupIdLevel_Missing + 2) {
  				iStartIndex = 0;
  				sGroupIdAtLevel = undefined;
  			} else if (iLevel == iGroupIdLevel_Missing + 1) {
  				iStartIndex = oGroupExpansionFirstMissingMember.startIndex_Missing;
  				sGroupIdAtLevel = oGroupExpansionFirstMissingMember.groupId_Missing;
  			} else if (iGroupIdLevel_Missing > 0) {
  				if (iLevel == iGroupIdLevel_Missing) {
  					sGroupIdAtLevel = oGroupExpansionFirstMissingMember.groupId_Missing;
  				} else {
  					sGroupIdAtLevel = this._getGroupIdAncestors(oGroupExpansionFirstMissingMember.groupId_Missing, -(iGroupIdLevel_Missing - iLevel))[0];
  				}
  				var sGroupIdAtParentLevel = this._getGroupIdAncestors(oGroupExpansionFirstMissingMember.groupId_Missing, -(iGroupIdLevel_Missing - iLevel + 1))[0];
  				if (!sGroupIdAtParentLevel) {
  					oLogger.fatal("failed to determine group id at parent level; group ID = " + sGroupId + ", level = " + iLevel);
  				}
  				iStartIndex = this._findKeyIndex(sGroupIdAtParentLevel, this.mEntityKey[sGroupIdAtLevel]);
  				if (iStartIndex == -1) {
  					oLogger.fatal("failed to determine position of value " + sGroupIdAtLevel + " in group " + sGroupIdAtParentLevel);
  				}
  				sGroupIdAtLevel = sGroupIdAtParentLevel;
  				iStartIndex++; // point to first missing position
  			}
  
  			// determine other parameters of the request
  			var iLengthForLevel = iLength > iLevel ? Math.ceil((iLength - iLevel) / (iAutoExpandGroupsToLevel - iLevel + 1)) : iLength;
  			var oLevelFilter = aFilterArray[iLevel - 1];
  
  			if (this.bUseAcceleratedAutoExpand) {
  				var oLevelMembersRequestDetails = prepareLevelMembersQueryRequest(AnalyticalBinding._requestType.levelMembersQuery, sGroupId,
  						iLevel, oLevelFilter, iStartIndex, iLengthForLevel, false, oLevelFilter == null ? true : false); // rem: bUseStartIndexForSkip==false, because it is encoded in the filter condition
  				oLevelMembersRequestDetails.sGroupId_Missing_AtLevel = sGroupIdAtLevel; // also remember group ID at the current level; needed for processing responses
  				oLevelMembersRequestDetails.sRequestId = this._getRequestId(AnalyticalBinding._requestType.levelMembersQuery, { groupId: sGroupId, level: iLevel });
  				aGroupMembersAutoExpansionRequestDetails.push(oLevelMembersRequestDetails);
  				aRequestId.push(oLevelMembersRequestDetails.sRequestId);
  			} else if (oLevelFilter && oLevelFilter.aFilters.length > 0) {
  				if (!oLevelFilter._bMultiFilter || oLevelFilter.bAnd) { // TODO remove this test once impl got mature to get rid of access to internal member; it is a consistency check if break-up will deliver expected results...
  					oLogger.fatal("level filter in wrong shape; cannot break it up");
  				}
  				for (var i = 0; i < oLevelFilter.aFilters.length; i++) { // break up level filter into its tuple filters combined with logical OR
  					var oTupleFilter = oLevelFilter.aFilters[i];
  					var oLevelMembersRequestDetails2 = prepareLevelMembersQueryRequest(AnalyticalBinding._requestType.levelMembersQuery, sGroupId,
  							iLevel, oTupleFilter, iStartIndex, iLengthForLevel, false, oLevelFilter == null ? true : false); // rem: bUseStartIndexForSkip==false, because it is encoded in the filter condition
  					oLevelMembersRequestDetails2.sGroupId_Missing_AtLevel = sGroupIdAtLevel; // also remember group ID at the current level; needed for processing responses
  					oLevelMembersRequestDetails2.sRequestId = this._getRequestId(AnalyticalBinding._requestType.levelMembersQuery, { groupId: sGroupId, level: iLevel, tupleIndex: i });
  					aGroupMembersAutoExpansionRequestDetails.push(oLevelMembersRequestDetails2);
  					aRequestId.push(oLevelMembersRequestDetails2.sRequestId);
  				}
  			} else { // no level filter given, so no need to break up anything, hence a single request is sufficient for this level
  				var oLevelMembersRequestDetails3 = prepareLevelMembersQueryRequest(AnalyticalBinding._requestType.levelMembersQuery, sGroupId,
  						iLevel, null /*oLevelFilter*/, iStartIndex, iLengthForLevel, false, oLevelFilter == null ? true : false); // rem: bUseStartIndexForSkip==false, because it is encoded in the filter condition
  				oLevelMembersRequestDetails3.sGroupId_Missing_AtLevel = sGroupIdAtLevel; // also remember group ID at the current level; needed for processing responses
  				oLevelMembersRequestDetails3.sRequestId = this._getRequestId(AnalyticalBinding._requestType.levelMembersQuery, { groupId: sGroupId, level: iLevel });
  				aGroupMembersAutoExpansionRequestDetails.push(oLevelMembersRequestDetails3);
  				aRequestId.push(oLevelMembersRequestDetails3.sRequestId);
  			}
  		}
  		return {
  			iRequestType : iRequestType,
  			aRequestId : aRequestId,
  			aGroupMembersAutoExpansionRequestDetails : aGroupMembersAutoExpansionRequestDetails,
  			sGroupId : sGroupId,
  			iLength : iLength
  		};
  	};
  
  	/**
  	 * @private
  	 */
  	AnalyticalBinding.prototype._prepareReloadMeasurePropertiesQueryRequest = function(iRequestType, oGroupMembersRequestDetails, oMultiUnitRepresentative) {
  		// build OData request based on given request details
  
  		// (1) set up analytical OData request object
  		var oAnalyticalQueryRequest = new odata4analytics.QueryResultRequest(this.oAnalyticalQueryResult);
  		oAnalyticalQueryRequest.setResourcePath(this._getResourcePath());
  		oAnalyticalQueryRequest.getSortExpression().clear();
  
  		// (2) set aggregation level
  		var aAggregationLevel = oGroupMembersRequestDetails.aAggregationLevel;
  		oAnalyticalQueryRequest.setAggregationLevel(aAggregationLevel);
  
  		var bIsLeafGroupsRequest = oGroupMembersRequestDetails.bIsLeafGroupsRequest;
  
  		// (3) set filter
  		var oFilterExpression = oAnalyticalQueryRequest.getFilterExpression();
  		oFilterExpression.clear();
  		if (this.aApplicationFilter) {
  			oFilterExpression.addUI5FilterConditions(this.aApplicationFilter);
  		}
  		if (this.aControlFilter) {
  			oFilterExpression.addUI5FilterConditions(this.aControlFilter);
  		}
  			// add conditions for aggregated dimension key
  		var aAggregationDimensionKeyFilter = [];
  		for (var i = 0; i < aAggregationLevel.length; i++) {
  			var oFilter = new Filter(aAggregationLevel[i], FilterOperator.EQ, oMultiUnitRepresentative.oEntry[aAggregationLevel[i]]);
  			aAggregationDimensionKeyFilter.push(oFilter);
  		}
  		oFilterExpression.addUI5FilterConditions(aAggregationDimensionKeyFilter);
  
  		// (4) set measures as requested per column
  		var bIncludeRawValue;
  		var bIncludeFormattedValue;
  		var bIncludeUnitProperty;
  		var oMeasureDetails;
  
  		var aSelectedUnitPropertyName = [];
  
  		// consider only those mentioned in oMultiUnitRepresentative.aReloadMeasurePropertyName
  		oAnalyticalQueryRequest.setMeasures(oMultiUnitRepresentative.aReloadMeasurePropertyName);
  
  		for ( var sMeasureName in this.oMeasureDetailsSet) {
  			oMeasureDetails = this.oMeasureDetailsSet[sMeasureName];
  			if (!oMultiUnitRepresentative.aReloadMeasurePropertyName || oMultiUnitRepresentative.aReloadMeasurePropertyName.indexOf(oMeasureDetails.name) == -1) {
  				continue;
  			}
  			if (!bIsLeafGroupsRequest && this.mAnalyticalInfoByProperty[sMeasureName].total == false) {
  				bIncludeRawValue = false;
  				bIncludeFormattedValue = false;
  				bIncludeUnitProperty = false;
  			} else {
  				bIncludeRawValue = (oMeasureDetails.rawValuePropertyName != undefined);
  				bIncludeFormattedValue = (oMeasureDetails.formattedValuePropertyName != undefined);
  				bIncludeUnitProperty = (oMeasureDetails.unitPropertyName != undefined);
  				if (bIncludeUnitProperty) {
  					// remember unit property together with using measure raw value property for response analysis in success handler
  					if (aSelectedUnitPropertyName.indexOf(oMeasureDetails.unitPropertyName) == -1) {
  						aSelectedUnitPropertyName.push(oMeasureDetails.unitPropertyName);
  					}
  				}
  			}
  			oAnalyticalQueryRequest.includeMeasureRawFormattedValueUnit(oMeasureDetails.name, bIncludeRawValue,
  					bIncludeFormattedValue, bIncludeUnitProperty);
  		}
  		// exclude those unit properties from the selected that are included in the current aggregation level
  		for ( var j in aAggregationLevel) {
  			var iMatchingIndex;
  			if ((iMatchingIndex = aSelectedUnitPropertyName.indexOf(aAggregationLevel[j])) != -1) {
  				aSelectedUnitPropertyName.splice(iMatchingIndex, 1);
  			}
  		}
  
  		return {
  			iRequestType : iRequestType,
  			sRequestId : this._getRequestId(AnalyticalBinding._requestType.reloadMeasuresQuery, {multiUnitEntryKey: this.oModel.getKey(oMultiUnitRepresentative.oEntry)}),
  			oAnalyticalQueryRequest : oAnalyticalQueryRequest,
  			aSelectedUnitPropertyName : aSelectedUnitPropertyName,
  			aAggregationLevel : aAggregationLevel,
  			oMultiUnitRepresentative : oMultiUnitRepresentative
  		};
  
  	};
  
  	/**
  	 * @private
  	 */
  	AnalyticalBinding.prototype._prepareGroupMembersAutoExpansionRequestIds = function(sGroupId, iNumberOfExpandedLevels) {
  		// intention of this function is to encapsulate the knowledge about steps to be taken
  		// for creating request IDs for all relevant requests
  		var iMinRequiredLevel = this._getGroupIdLevel(sGroupId) + 1;
  		var iAutoExpandGroupsToLevel = iMinRequiredLevel + iNumberOfExpandedLevels;
  		var aRequestId = [];
  		for (var iLevel = iMinRequiredLevel; iLevel <= iAutoExpandGroupsToLevel; iLevel++) {
  			aRequestId.push(this._getRequestId(AnalyticalBinding._requestType.levelMembersQuery, { groupId: sGroupId, level: iLevel }));
  		}
  		return aRequestId;
  	};
  
  	/**
  	 * @param {boolean} bAddAdditionalSelects
  	 *   Whether additional selects, computed from select binding parameter, shall be added to the
  	 *   $select query option.
  	 * @private
  	 */
  	AnalyticalBinding.prototype._getQueryODataRequestOptions = function(oAnalyticalQueryRequest,
  			bAddAdditionalSelects, mParameters) {
  		var i;
  
  		mParameters = mParameters || {};
  
  		try {
  			oAnalyticalQueryRequest.getFilterExpression().checkValidity(); // fails if false
  		} catch (e) {
  			oLogger.fatal("filter expression is not valid", e.toString());
  			return undefined;
  		}
  
  		var sSelect = oAnalyticalQueryRequest.getURIQueryOptionValue("$select");
  		var sFilter = oAnalyticalQueryRequest.getURIQueryOptionValue("$filter");
  		var sOrderBy = oAnalyticalQueryRequest.getURIQueryOptionValue("$orderby");
  		var sSkip = oAnalyticalQueryRequest.getURIQueryOptionValue("$skip");
  		var sTop = oAnalyticalQueryRequest.getURIQueryOptionValue("$top");
  		var sInlineCount = oAnalyticalQueryRequest.getURIQueryOptionValue("$inlinecount");
  
  		if (bAddAdditionalSelects && this.aAdditionalSelects.length > 0) {
  			sSelect = (sSelect ? sSelect.split(",") : [])
  				.concat(this.aAdditionalSelects).join(",");
  		}
  
  		if (this.mParameters && this.mParameters["filter"]) {
  			if (sFilter === null) {
  				sFilter = this.mParameters["filter"];
  			} else {
  				sFilter += "and (" + this.mParameters["filter"] + ")";
  			}
  		}
  
  		// construct OData request option parameters
  		var aParam = [];
  		if (sSelect !== null) {
  			aParam.push("$select=" + sSelect);
  		}
  		if (sFilter !== null) {
  			aParam.push("$filter=" + sFilter);
  		}
  		if (sOrderBy !== null) {
  			aParam.push("$orderby=" + sOrderBy);
  		}
  		if (sSkip !== null) {
  			aParam.push("$skip=" + sSkip);
  		}
  		if (sTop !== null) {
  			aParam.push("$top=" + sTop);
  		}
  		if (sInlineCount !== null) {
  			aParam.push("$inlinecount=" + sInlineCount);
  		}
  
  		//encode if necessary
  		if (mParameters.encode === true) {
  			for (i = 0; i < aParam.length; i++) {
  				aParam[i] = aParam[i].replace(/\ /g, "%20");
  			}
  		}
  
  		return aParam;
  	};
  
  	/**
  	 * @private
  	 */
  	AnalyticalBinding.prototype._executeBatchRequest = function(aRequestDetails) {
  		var iCurrentAnalyticalInfoVersion = this.iAnalyticalInfoVersionNumber,
  			iRequestHandleId,
  			that = this;
  
  		var aBatchQueryRequest = [], aExecutedRequestDetails = [];
  
  		function triggerDataReceived() {
  			that.fireDataReceived({__simulateAsyncAnalyticalBinding: true});
  		}
  
  		// Batch Response Handling for ODataModel V2
  		var oResponseCollector = new BatchResponseCollector();
  		//Sucess handler called by the ODataModel for each batch sub-request
  		function fnSingleBatchSucess(oData, oResponse) {
  			oResponseCollector.success(oResponse);
  		}
  		//same as with success
  		function fnSingleBatchError(oData, oResponse) {
  			oResponseCollector.error(oResponse || oData);
  		}
  
  		// BCP: 1770008178
  
  		// Legacy Support:
  		// We set the bNeedsUpdate flag to "true" if ALL request details are empty.
  		// This happens when the initial analyticalInfo is empty and is not set correctly
  		// before the control calls getRootContexts etc.
  		// If at a later point the analyticalInfo is correctly set AND the bNeedsUpdate flag is still true
  		// we falsly force a change event in checkUpdate --> this might lead to an unnecessary re-rendering of the control.
  
  		// In case we have at least 1 valid request (including measures and/or dimensions)
  		// we set the bNeedsUpdate flag to false, because the update flag is set to true ANYWAY during the response-processing.
  		this.bNeedsUpdate = true;
  		for (var iDetail = 0; iDetail < aRequestDetails.length; iDetail++) {
  			var oDetail = aRequestDetails[iDetail];
  			if (oDetail.aAggregationLevel && oDetail.aAggregationLevel.length > 0) {
  				this.bNeedsUpdate = false;
  			}
  		}
  
  		//create sub-requests for all defined requestDetails
  		for (var i = -1, oRequestDetails; (oRequestDetails = aRequestDetails[++i]) !== undefined;) {
  			var oAnalyticalQueryRequest = oRequestDetails.oAnalyticalQueryRequest, sGroupId = oRequestDetails.sGroupId;
  
  			if (oAnalyticalQueryRequest.getURIQueryOptionValue("$select") == null) {
  				// no dimensions and no measures requested, so create an artificial empty root context (synonym for the regular "/")
  				this.fireDataRequested({__simulateAsyncAnalyticalBinding: true}); // simulate the async behavior
  
  				// perform all steps of fct fnSuccess (w/o calling it, b/c its argument is some data object and not a context
  				sGroupId = null;
  				this.mServiceLength[sGroupId] = this.mLength[sGroupId] = 1;
  				this.mServiceFinalLength[sGroupId] = true;
  				this._setServiceKey(this._getKeyIndexMapping(sGroupId, 0), AnalyticalBinding._artificialRootContextGroupId);
  				// BCP: 1770008178, see comment above
  				// this.bNeedsUpdate = true;
  				// simulate the async behavior, dataRequested and dataReceived have to be fired in pairs
  				setTimeout(triggerDataReceived);
  
  				this.bArtificalRootContext = true;
  				// return immediately - no need to load data...
  				continue;
  			}
  			var sPath = oAnalyticalQueryRequest.getURIToQueryResultEntries();
  
  			//ensure absolute path if no context is set
  			if (!this.oContext && sPath[0] !== "/") {
  				sPath = "/" + sPath;
  			}
  			/*
  			 * This might be needed, as soon as the AnalyticalBinding can handle relative binding
  			 * @see odata4analytics -> getRequestURi... and _getResourcePath -> enforces always an absolute path
  			 * else if (this.oContext && sPath[0] === "/") {
  				sPath = sPath.substring(1);
  			}*/
  			if (!this._isRequestPending(oRequestDetails.sRequestId)) {
  				/* side note: the check for a pending request is repeated at this point (first check occurs in _getContextsForParentGroupId),
  				   because the logic executed for a call to the binding API may yield to identical OData requests in a single batch.
  				   Since _processRequestQueue, and hence also _executeBatchRequest are executed asynchronously, this method is the first place
  				   where the set of all operations included in the batch request becomes known and this condition can be checked. */
  				this._registerNewRequest(oRequestDetails.sRequestId);
  
  				if (this.iModelVersion === AnalyticalVersionInfo.V1) {
  					//V1 - use createBatchOperation
  					aBatchQueryRequest.push(this.oModel.createBatchOperation(sPath.replace(/\ /g, "%20"), "GET"));
  				}else if (this.iModelVersion === AnalyticalVersionInfo.V2) {
  					var aUrlParameters = this._getQueryODataRequestOptions(oAnalyticalQueryRequest,
  							oRequestDetails.bIsLeafGroupsRequest,  {encode: true});
  					if (this.sCustomParams) {
  						aUrlParameters.push(this.sCustomParams);
  					}
  					//V2 - use read()
  					var oRequestHandle = this.oModel.read(sPath.replace(/\ /g, "%20"), {
  						success: fnSingleBatchSucess, // relays the success to the BatchResponseCollector
  						error: fnSingleBatchError,
  						context: this.oContext,
  						urlParameters: aUrlParameters
  					});
  					aBatchQueryRequest.push(oRequestHandle);
  				}
  				aExecutedRequestDetails.push(oRequestDetails);
  			}
  		}
  
  		//var iRequestHandleId = this._getIdForNewRequestHandle();
  		if (aBatchQueryRequest.length > 0) {
  			oLogger.debug("AnalyticalBinding: executing batch request with " + aExecutedRequestDetails.length + " operations");
  //			this._trace_message("ReqExec", "submitting batch with " + aExecutedRequestDetails.length + " operations");
  
  			var oBatchRequestHandle;
  
  			iRequestHandleId = this._getIdForNewRequestHandle();
  
  			// fire events to indicate sending of a new request
  			this.fireDataRequested();
  
  			if (this.iModelVersion === AnalyticalVersionInfo.V1) {
  				this.oModel.addBatchReadOperations(aBatchQueryRequest);
  				oBatchRequestHandle = this.oModel.submitBatch(fnSuccess, fnError, true, true);
  
  				this.oModel.fireRequestSent({url : this.oModel.sServiceUrl + "/$batch", type : "POST", async : true,
  					info: "",
  					infoObject : {}
  				});
  			} else {
  				// fake a uniform request handle, so the original code works with the v2 ODataModel
  				// the v2 model does not return an overall request handle for the batch request
  				oBatchRequestHandle = {
  					abort: function() {
  						//relay the abort call to all sub-requests created by v2.ODataModel.read()
  						for (var iRequestIndex = 0; iRequestIndex < aBatchQueryRequest.length; iRequestIndex++) {
  							aBatchQueryRequest[iRequestIndex].abort();
  						}
  					}
  				};
  				// The response collector keeps track of all returned requests and
  				// calls the original success/error handlers after all batch responses have returned
  				oResponseCollector.setup({
  					executedRequests: aExecutedRequestDetails,
  					binding: this,
  					success: fnSuccess,
  					error: fnError
  				});
  			}
  
  			this._registerNewRequestHandle(iRequestHandleId, oBatchRequestHandle);
  		}
  
  		function fnSuccess(oData, response) {
  			that._deregisterHandleOfCompletedRequest(iRequestHandleId);
  
  			if (aExecutedRequestDetails.length != oData.__batchResponses.length) {
  				oLogger.fatal("assertion failed: received " + oData.__batchResponses.length
  						+ " responses for " + aExecutedRequestDetails.length + " read operations in the batch request");
  			}
  
  			if (iCurrentAnalyticalInfoVersion != that.iAnalyticalInfoVersionNumber) {
  				// discard responses for outdated analytical infos
  				for (var j = 0; j < aExecutedRequestDetails.length; j++) {
  					var sRequestId = aExecutedRequestDetails[j].sRequestId;
  					if (sRequestId !== undefined) {
  						that._deregisterCompletedRequest(sRequestId);
  						that._cleanupGroupingForCompletedRequest(sRequestId);
  					}
  				}
  				that.fireDataReceived({data: []});
  				return;
  			}
  
  			var iEmptyResults = 0;
  			for (var k = 0; k < oData.__batchResponses.length; k++) {
  				if (oData.__batchResponses[k].data != undefined) {
  					//check for empty results
  					if (oData.__batchResponses[k].data.results.length == 0) {
  						iEmptyResults++;
  					}
  					switch (aExecutedRequestDetails[k].iRequestType) {
  						case AnalyticalBinding._requestType.groupMembersQuery:
  							that._processGroupMembersQueryResponse(aExecutedRequestDetails[k], oData.__batchResponses[k].data);
  							break;
  						case AnalyticalBinding._requestType.totalSizeQuery:
  							that._processTotalSizeQueryResponse(aExecutedRequestDetails[k], oData.__batchResponses[k].data);
  							break;
  						case AnalyticalBinding._requestType.levelMembersQuery:
  							that._processLevelMembersQueryResponse(aExecutedRequestDetails[k], oData.__batchResponses[k].data);
  							break;
  						case AnalyticalBinding._requestType.reloadMeasuresQuery:
  							that._processReloadMeasurePropertiesQueryResponse(aExecutedRequestDetails[k], oData.__batchResponses[k].data);
  							break;
  						default:
  							oLogger.fatal("invalid request type " + aExecutedRequestDetails[k].iRequestType);
  							continue;
  					}
  				}
  				that._deregisterCompletedRequest(aExecutedRequestDetails[k].sRequestId);
  				that._cleanupGroupingForCompletedRequest(aExecutedRequestDetails[k].sRequestId);
  			}
  
  			// if all results are empty and the request was an auto-expand request, the length has to be set to final and 0
  			if (that.mParameters && that.mParameters.numberOfExpandedLevels > 0) {
  				if (iEmptyResults == oData.__batchResponses.length) {
  					that.mLength["/"] = 0;
  					that.mFinalLength["/"] = true;
  				}
  			}
  
  			// determine the logical success status: true iff all operations succeeded
  			var bOverallSuccess = true;
  			var aBatchErrors;
  
  			// raise event here since there is no separate fnCompleted handler for batch requests
  			that.fireDataReceived({data: oData});
  
  			//check for possible V1 errors
  			var oV1Errors = {};
  			if (that.iModelVersion === AnalyticalVersionInfo.V1) {
  				// retrieve the errors from the model and reset the success flag
  				aBatchErrors = that.oModel._getBatchErrors(oData);
  				if (aBatchErrors.length > 0) {
  					bOverallSuccess = false;
  					oV1Errors = that.oModel._handleError(aBatchErrors[0]);
  				}
  
  				// fire event to indicate completion of request
  				that.oModel.fireRequestCompleted({url : response.requestUri, type : "POST", async : true,
  					info: "",
  					infoObject : {},
  					success: bOverallSuccess,
  					errorobject: bOverallSuccess ? {} : oV1Errors
  				});
  				// notify all bindings of the model that the data has been changed!
  				// e.g. controls in the rows need to be updated as well
  				// fire only the change event is not sufficient for other bindings
  				if (bOverallSuccess) {
  					that.oModel.checkUpdate();
  				}
  			}
  		}
  
  		function fnError (oError) {
  			// in case the error is triggered by an aborted request, don't cleanup the handle queue, as it is already cleaned-up by the abort call.
  			if (oError && oError.statusText != "abort") {
  				that._deregisterHandleOfCompletedRequest(iRequestHandleId);
  				for (var j = -1, oExecutedRequestDetails; (oExecutedRequestDetails = aExecutedRequestDetails[++j]) !== undefined;) {
  					that._deregisterCompletedRequest(oExecutedRequestDetails.sRequestId);
  					that._cleanupGroupingForCompletedRequest(oExecutedRequestDetails.sRequestId);
  				}
  			}
  			if (iCurrentAnalyticalInfoVersion != that.iAnalyticalInfoVersionNumber) {
  				// discard responses for outdated analytical infos
  				return;
  			}
  
  			var oV1Error = oError;
  			if (that.iModelVersion === AnalyticalVersionInfo.V1) {
  				oV1Error = that.oModel._handleError(oError);
  			}
  
  			// fire event to indicate completion of request
  			that.oModel.fireRequestCompleted({url : "", type : "POST", async : true,
  				info: "",
  				infoObject : {},
  				success: false,
  				errorobject: oV1Error});
  
  			// Legacy Code: Unsure if this is need for OData V1 Model...
  			if (that.iModelVersion === AnalyticalVersionInfo.V1) {
  				that.oModel.fireRequestFailed(oV1Error);
  			}
  
  			that.fireDataReceived();
  		}
  	};
  
  	/**
  	 * @private
  	 */
  	AnalyticalBinding.prototype._executeQueryRequest = function(oRequestDetails) {
  		if (oRequestDetails.iRequestType == AnalyticalBinding._requestType.groupMembersAutoExpansionQuery) {
  			// handle auto-expanding requests that are actually a bundle of multiple requests, one per level
  			for (var i = -1, oAnalyticalQueryRequest2; (oAnalyticalQueryRequest2 = oRequestDetails.aGroupMembersAutoExpansionRequestDetails[++i]) !== undefined; ) {
  				this._executeQueryRequest(oAnalyticalQueryRequest2);
  			}
  			return;
  		}
  
  		var iCurrentAnalyticalInfoVersion = this.iAnalyticalInfoVersionNumber;
  
  		var oAnalyticalQueryRequest = oRequestDetails.oAnalyticalQueryRequest, sGroupId = oRequestDetails.sGroupId;
  
  		// determine relevant request query options
  		var sPath = oAnalyticalQueryRequest.getURIToQueryResultEntitySet();
  		var aParam = this._getQueryODataRequestOptions(oAnalyticalQueryRequest,
  				oRequestDetails.bIsLeafGroupsRequest);
  
  		if (!aParam) {
  			// parameters could not be determined correctly
  			return;
  		}
  
  		var that = this;
  
  		if (oAnalyticalQueryRequest.getURIQueryOptionValue("$select") == null) {
  			// no dimensions and no measures requested, so create an artificial empty root context (synonym for the regular "/")
  			this.fireDataRequested({__simulateAsyncAnalyticalBinding: true}); // simulate the async behavior
  
  			// perform all steps of fct fnSuccess (w/o calling it, b/c its argument is some data object and not a context
  			sGroupId = null;
  			this.mServiceLength[sGroupId] = this.mLength[sGroupId] = 1;
  			this.mServiceFinalLength[sGroupId] = true;
  			this._setServiceKey(this._getKeyIndexMapping(sGroupId, 0), AnalyticalBinding._artificialRootContextGroupId);
  			this.bNeedsUpdate = true;
  			// simulate the async behavior for the root context in case of having no sums (TODO: reconsider!)
  			setTimeout(function() {
  				if (that._cleanupGroupingForCompletedRequest(oRequestDetails.sRequestId)) {
  					that.fireDataReceived({__simulateAsyncAnalyticalBinding: true});
  				}
  			});
  			this.bArtificalRootContext = true;
  			// return immediately - no need to load data...
  			return;
  		}
  		this._registerNewRequest(oRequestDetails.sRequestId);
  		// execute the request and use the metadata if available
  		this.fireDataRequested();
  		for (var j = 0; j < aParam.length; j++) {
  			aParam[j] = aParam[j].replace(/\ /g, "%20");
  		}
  		oLogger.debug("AnalyticalBinding: executing query request");
  
  		var iRequestHandleId = this._getIdForNewRequestHandle();
  		if (this.iModelVersion === AnalyticalVersionInfo.V1) {
  			//trigger data loading, the request handle is registered during the fnHandleUpdate callback, used by the V1 model
  			this.oModel._loadData(sPath, aParam, fnSuccess, fnError, false, fnUpdateHandle, fnCompleted);
  		} else {
  			if (this.sCustomParams) {
  				aParam.push(this.sCustomParams);
  			}
  			var oRequestHandle = this.oModel.read(sPath, {
  				success: fnSuccess,
  				error: fnError,
  				context: this.oContext,
  				urlParameters: aParam
  			});
  			//the handle has to be registered here, because the V2 model does not support an fnHandleUpdate callback anymore
  			that._registerNewRequestHandle(iRequestHandleId, oRequestHandle);
  		}
  
  		function fnSuccess(oData) {
  			that._deregisterHandleOfCompletedRequest(iRequestHandleId);
  
  			if (iCurrentAnalyticalInfoVersion != that.iAnalyticalInfoVersionNumber) {
  				// discard responses for outdated analytical infos
  				that._deregisterCompletedRequest(oRequestDetails.sRequestId);
  				return;
  			}
  			switch (oRequestDetails.iRequestType) {
  				case AnalyticalBinding._requestType.groupMembersQuery:
  					that._processGroupMembersQueryResponse(oRequestDetails, oData);
  					break;
  				case AnalyticalBinding._requestType.totalSizeQuery:
  					that._processTotalSizeQueryResponse(oRequestDetails, oData);
  					break;
  				case AnalyticalBinding._requestType.levelMembersQuery:
  					that._processLevelMembersQueryResponse(oRequestDetails, oData);
  					break;
  				case AnalyticalBinding._requestType.reloadMeasuresQuery:
  					that._processReloadMeasurePropertiesQueryResponse(oRequestDetails, oData);
  					break;
  				default:
  					oLogger.fatal("invalid request type " + oRequestDetails.iRequestType);
  					break;
  			}
  			that._deregisterCompletedRequest(oRequestDetails.sRequestId);
  
  			// with ODataModel V2, the completed function is not called by the model anymore
  			// the correct moment to clean up is after the success handler
  			// the error handler takes care of this itself
  			if (that.iModelVersion === AnalyticalVersionInfo.V2) {
  				fnCompleted(oData);
  			}
  		}
  
  		function fnCompleted(oData) {
  			if (iCurrentAnalyticalInfoVersion != that.iAnalyticalInfoVersionNumber) {
  				// discard responses for outdated analytical infos
  				return;
  			}
  			if (that._cleanupGroupingForCompletedRequest(oRequestDetails.sRequestId)) {
  				that.fireDataReceived({data: oData});
  			}
  		}
  
  		function fnError(oError) {
  			// in case the error is triggered by an aborted request, don't cleanup the request-handle queue, as it is already cleaned-up by the abort call
  			if (oError && oError.statusText == "abort") {
  				that.fireDataReceived();
  				return;
  			}
  
  			that._deregisterHandleOfCompletedRequest(iRequestHandleId);
  			that._deregisterCompletedRequest(oRequestDetails.sRequestId);
  			that._cleanupGroupingForCompletedRequest(oRequestDetails.sRequestId);
  
  			if (iCurrentAnalyticalInfoVersion != that.iAnalyticalInfoVersionNumber) {
  				// discard responses for outdated analytical infos
  				return;
  			}
  			that.fireDataReceived();
  		}
  
  		function fnUpdateHandle(oRequestHandle) {
  			that._registerNewRequestHandle(iRequestHandleId, oRequestHandle);
  		}
  
  	};
  
  	/**
  	 * @private
  	 */
  	AnalyticalBinding.prototype._abortAllPendingRequests = function() {
  		this._abortAllPendingRequestsByHandle();
  		this._clearAllPendingRequests();
  	};
  
  	/**
  	 * @private
  	 */
  	AnalyticalBinding.prototype._processGroupMembersQueryResponse = function(oRequestDetails, oData) {
  		var sEntryGroupId,
  			sGroupId = oRequestDetails.sGroupId,
  			bHasSorters = this.aSorter.length > 0, // this.aSorter is always an array
  			aSelectedUnitPropertyName = oRequestDetails.aSelectedUnitPropertyName,
  			aAggregationLevel = oRequestDetails.aAggregationLevel,
  			iStartIndex = oRequestDetails.oKeyIndexMapping.iIndex,
  			iServiceStartIndex = oRequestDetails.oKeyIndexMapping.iServiceKeyIndex,
  			iLength = oRequestDetails.iLength,
  			oKeyIndexMapping = oRequestDetails.oKeyIndexMapping,
  			iGroupMembersLevel = sGroupId == null ? 0 : this._getGroupIdLevel(sGroupId) + 1,
  			bUnitCheckRequired = (aSelectedUnitPropertyName.length > 0),
  			sPreviousEntryDimensionKeyString, sDimensionKeyString,
  			iFirstMatchingEntryIndex,
  			iDiscardedEntriesCount = 0,
  			bLastServiceKeyWasNew,
  			oReloadMeasuresRequestDetails, aReloadMeasuresRequestDetails = [];
  
  // 		this._trace_enter("ReqExec", "_processGroupMembersQueryResponse", "groupId=" + oRequestDetails.sGroupId, { startIndex: iStartIndex, serviceStartIndex: iServiceStartIndex, length: iLength, resultCount: oData.__count, resultLength: oData.results.length }, ["startIndex","serviceStartIndex","length","resultCount","resultLength"]); // DISABLED FOR PRODUCTION
  
  		// entry at start position may be a multi-unit entry w.r.t. entry at position before
  		// prepare merging with this preceding entry
  		var iODataResultsLength = oData.results.length;
  
  		if (sGroupId === null && iODataResultsLength > 1 && this._canApplySortersToGroups()) {
  			this._warnNoSortingOfGroups(bHasSorters ? "binding is refreshed" : undefined);
  			if (bHasSorters) {
  				// do refresh after _executeQueryRequest is finished to avoid an error while
  				// cleaning the pending request queue (see _deregisterCompletedRequest)
  				setTimeout(this.refresh.bind(this), 0);
  				return;
  			}
  		}
  
  		var aPreviousEntryServiceKey = this._getServiceKeys(sGroupId, oKeyIndexMapping.iIndex - 1);
  		sPreviousEntryDimensionKeyString = undefined;
  		if (aPreviousEntryServiceKey && aPreviousEntryServiceKey.length > 0) { // copy previous service keys to results for homogeneous processing below
  			for (var i = 0, len = aPreviousEntryServiceKey.length; i < len; i++) {
  				oData.results[i - len] = this.oModel.getObject("/" + aPreviousEntryServiceKey[i]);
  			}
  			var oEntry2 = oData.results[-aPreviousEntryServiceKey.length];
  			sPreviousEntryDimensionKeyString = "";
  			for (var j = 0; j < aAggregationLevel.length; j++) {
  				sPreviousEntryDimensionKeyString += oEntry2[aAggregationLevel[j]] + "|";
  			}
  		}
  
  		// special case: previous entry gets merged with the first loaded entry/ies
  		// if there was a single preceding entity that was not yet a multi-unit entry, count it!
  		// (on the opposite, if there were multiple, hence multi-unit entries, they were counted already when they got loaded)
  		// Therefore, in order to count it, the bLastServiceKeyWasNew flag is set accordingly
  		bLastServiceKeyWasNew = aPreviousEntryServiceKey && aPreviousEntryServiceKey.length == 1;
  
  		// process loaded data, collect contexts and handle multi-unit occurrences; oKeyIndexMapping points to the current insert positions for key indexes and service keys
  		for (var h = 0; h < iODataResultsLength; h++) {
  			var oEntry = oData.results[h];
  
  			if (bUnitCheckRequired) {
  				// perform check to detect multiple returned entries for a single group level instance; duplicates are detected by having the same dimension keys
  				sDimensionKeyString = "";
  				for (var g = 0; g < aAggregationLevel.length; g++) {
  					sDimensionKeyString += oEntry[aAggregationLevel[g]] + "|";
  				}
  				if (sPreviousEntryDimensionKeyString == sDimensionKeyString) {
  					this._warnNoSortingOfGroups();
  					if (iFirstMatchingEntryIndex === undefined) {
  						if (h == 0) { // adjust indexes such that the entry at position before is covered
  							iFirstMatchingEntryIndex = -aPreviousEntryServiceKey.length;
  							oKeyIndexMapping.iServiceKeyIndex -= aPreviousEntryServiceKey.length - 1; // must point to the second entry with the same dimension key
  // 							// this._trace_debug_if(aPreviousEntryServiceKey.length > 1,"one or more loaded entries will get merged with preceding multi-unit entry");
  						} else {
  							iFirstMatchingEntryIndex = h - 1;
  						}
  					}
  					var iDeviatingUnitPropertyNameIndex = -1, oPreviousEntry = oData.results[h - 1];
  					for (var k = 0; k < aSelectedUnitPropertyName.length; k++) {
  						if (oPreviousEntry[aSelectedUnitPropertyName[k]] != oEntry[aSelectedUnitPropertyName[k]]) {
  							iDeviatingUnitPropertyNameIndex = k; // aggregating dimensions are all the same, entries only differ in currency
  							break;
  						}
  					}
  					if (iDeviatingUnitPropertyNameIndex == -1) {
  // 						this._trace_debug_if(true, "assertion failed: no deviating units found for result entries " + (h - 1) + " and " + h);
  						oLogger.fatal("assertion failed: no deviating units found for result entries " + (h - 1) + " and " + h, null, null, createSupportInfo(this, "NO_DEVIATING_UNITS"));
  					}
  				}
  				if ((sPreviousEntryDimensionKeyString != sDimensionKeyString || h == iODataResultsLength - 1)
  						&& iFirstMatchingEntryIndex !== undefined) { // after sequence of identical entries or if processing the last result entry (the set iFirstMatchingEntryIndex indicates the multi-unit case)
  /* multi-unit verification: remember if a multi-unit occurrence is found */
  /*start code block*
  					this.bFoundMU = true;
  *end code block*/
  					// collect all related result entries for this multi-unit entity and set the keys
  					var aMultiUnitEntry = [];
  					for (var l = iFirstMatchingEntryIndex; l < h; l++) {
  						aMultiUnitEntry.push(oData.results[l]);
  					}
  					if (sPreviousEntryDimensionKeyString == sDimensionKeyString) {
  						aMultiUnitEntry.push(oData.results[h]);
  					}
  					// determine all deviating unit properties
  					var aDeviatingUnitPropertyName = [];
  					for (var m = 0; m < aSelectedUnitPropertyName.length; m++) {
  						var sUnitPropertyName = aSelectedUnitPropertyName[m];
  						for (var o = 1; o < aMultiUnitEntry.length; o++) {
  							if (aMultiUnitEntry[o - 1][sUnitPropertyName] != aMultiUnitEntry[o][sUnitPropertyName]) {
  								aDeviatingUnitPropertyName.push(sUnitPropertyName);
  								break;
  							}
  						}
  					}
  					// create a multi-unit repr. (includes a corresponding multi-unit entity)
  					var oMultiUnitRepresentative = this._createMultiUnitRepresentativeEntry(sGroupId, oData.results[iFirstMatchingEntryIndex], aSelectedUnitPropertyName, aDeviatingUnitPropertyName, oRequestDetails.bIsFlatListRequest);
  					if (oMultiUnitRepresentative.aReloadMeasurePropertyName.length > 0) {
  						oReloadMeasuresRequestDetails = this._prepareReloadMeasurePropertiesQueryRequest(AnalyticalBinding._requestType.reloadMeasuresQuery, oRequestDetails, oMultiUnitRepresentative);
  						// only schedule reloadMeasure requests if there is something to select -> it might be that some measure could be reloaded, but the column
  						// might not be totaled (yet might be visible/inResult)
  						// BCP: 1570786546
  						if (oReloadMeasuresRequestDetails.oAnalyticalQueryRequest && oReloadMeasuresRequestDetails.oAnalyticalQueryRequest.getURIQueryOptionValue("$select") != null) {
  							aReloadMeasuresRequestDetails.push(oReloadMeasuresRequestDetails);
  						}
  					}
  					var iNewServiceKeyCount = this._setAdjacentMultiUnitKeys(oKeyIndexMapping, oMultiUnitRepresentative, aMultiUnitEntry);
  
  					// update number of discarded entities
  					var iMultiUnitEntryDiscardedEntriesCount;
  					if (oMultiUnitRepresentative.bIsNewEntry) {
  						iMultiUnitEntryDiscardedEntriesCount = aMultiUnitEntry.length - 1;
  					} else {
  						iMultiUnitEntryDiscardedEntriesCount = iNewServiceKeyCount;
  					}
  					if (bLastServiceKeyWasNew) {
  						bLastServiceKeyWasNew = false;
  					}
  					if (iMultiUnitEntryDiscardedEntriesCount < 0) {
  // 						this._trace_debug_if(iDiscardedEntriesCount < 0, "assertion failed: iDiscardedEntriesCount must be non-negative");
  						oLogger.fatal("assertion failed: iDiscardedEntriesCount must be non-negative");
  					}
  					iDiscardedEntriesCount += iMultiUnitEntryDiscardedEntriesCount;
  /* multi-unit verification: remember multi-unit key */
  /*start code block*
  					this.bNewMUKey = oMultiUnitRepresentative.bIsNewEntry;
  *end code block*/
  					// adjust mEntityKey for detected and handled multi-unit situation
  					var sMultiUnitKey = this.oModel._getKey(oMultiUnitRepresentative.oEntry);
  					var oMultiUnitContext = this.oModel.getContext('/' + sMultiUnitKey);
  					this._getGroupIdFromContext(oMultiUnitContext, iGroupMembersLevel);
  					this.mEntityKey[sEntryGroupId] = sMultiUnitKey;
  
  					// reset multi-unit indicator
  					iFirstMatchingEntryIndex = undefined;
  
  					// add current entry if it has different key combination
  					if (sPreviousEntryDimensionKeyString != sDimensionKeyString) {
  						bLastServiceKeyWasNew = this._setServiceKey(oKeyIndexMapping, this.oModel._getKey(oEntry));
  					}
  				} else if (sPreviousEntryDimensionKeyString != sDimensionKeyString) {
  					bLastServiceKeyWasNew = this._setServiceKey(oKeyIndexMapping, this.oModel._getKey(oEntry));
  				}
  /* multi-unit verification: remember if differend dimensions are involved - needed for correct index calculation */
  /*start code block*
  				this.bDiffDims = (sPreviousEntryDimensionKeyString != sDimensionKeyString);
  *end code block*/
  				sPreviousEntryDimensionKeyString = sDimensionKeyString;
  			} else {
  				this._setServiceKey(oKeyIndexMapping, this.oModel._getKey(oEntry));
  			}
  
  			// remember mapping between entry key and group Id
  			if (!oRequestDetails.bIsLeafGroupsRequest) {
  				var sLastEntryKey = this._getKey(sGroupId, oKeyIndexMapping.iIndex - 1);
  
  				sEntryGroupId = this._getGroupIdFromContext(this.oModel.getContext('/' + sLastEntryKey), iGroupMembersLevel);
  /* during development only
  				if (this.mEntityKey[sEntryGroupId]) {
  					if (this.mEntityKey[sEntryGroupId] != sLastEntryKey)
  						// Such errors will occur in case repeated calls for same groups with providers returned unstable entity keys
  						// E.g., HANA/XS does not provide stable keys. As s
  						// As soon as repetitive calls are avoided, such errors will vanish as well
  						oLogger.debug("unstable keys detected: group ID " + sEntryGroupId + " does not have a unique entity key");
  				}
  */
  				this.mEntityKey[sEntryGroupId] = sLastEntryKey;
  			}
  /* multi-unit verification: collect the cumulated number of discarded entries in a separate member array for analysis --- see below */
  /*start code block*
  			if (this.aDiscCount === undefined)	{
  				this.aDiscCount = [];
  				this.aCheckDiscCount = [];
  			}
  			if (h == 0) {
  				this.aDiscCount = [];
  				this.aCheckDiscCount = [];
  			}
  
  			if (this.bFoundMU) {
  				if (this.bDiffDims) {
  					this.aDiscCount[oKeyIndexMapping.iIndex - 2] = iDiscardedEntriesCount;
  					this.aDiscCount[oKeyIndexMapping.iIndex - 1] = iDiscardedEntriesCount;
  					this.aCheckDiscCount[oKeyIndexMapping.iIndex - 2] = this.bNewMUKey;
  				} else {
  					this.aDiscCount[oKeyIndexMapping.iIndex - 1] = iDiscardedEntriesCount;
  					this.aCheckDiscCount[oKeyIndexMapping.iIndex - 1] = this.bNewMUKey;
  				}
  				this.bFoundMU = false;
  				this.bDiffDims = false;
  				this.bNewMUKey = false;
  			} else {
  				this.aDiscCount[oKeyIndexMapping.iIndex - 1] = iDiscardedEntriesCount;
  			}
  *end code block*/
  		}
  /* multi-unit verification: perform check between created service indexes and count of discarded service entities */
  /*start code block*
  * eslint-disable no-debugger, no-unused-vars, no-empty *
  		for (var chkIndex = iStartIndex, iMaxIndex = oKeyIndexMapping.iIndex - 1; chkIndex <= iMaxIndex; chkIndex++) {
  			if (! this.aCheckDiscCount[chkIndex]) {
  				continue;
  			}
  			var iKeyIndex = this.mKeyIndex[sGroupId][chkIndex];
  			var iDiscCountAtChkIndex = this.aDiscCount[chkIndex];
  			var iDiscCountAtBefore = this.aDiscCount[chkIndex - 1] !== undefined ? this.aDiscCount[chkIndex - 1] : 0;
  			if (iKeyIndex >= 0) {
  // 				this._trace_debug_if(iDiscCountAtChkIndex - iDiscCountAtBefore != 0, "assertion failed: disc count == 0 at key index = " + (chkIndex));
  			} else {
  				if (iKeyIndex == "ZERO") {
  					iKeyIndex = 0;
  				}
  				var iNextKeyIndex = this.mKeyIndex[sGroupId][chkIndex + 1];
  				if (iNextKeyIndex !== undefined) {
  					var iDistanceToNextKeyIndex = Math.abs(iNextKeyIndex) - Math.abs(iKeyIndex);
  // 					this._trace_debug_if(iDistanceToNextKeyIndex - 1 != iDiscCountAtChkIndex - iDiscCountAtBefore, "assertion failed: disc count exp = " + (iDistanceToNextKeyIndex - 1) + ", but got " + (iDiscCountAtChkIndex - iDiscCountAtBefore) + " at key index = " + (chkIndex));
  				} else {
  					var cnt = 0, ii = Math.abs(iKeyIndex);
  					while (this.mServiceKey[sGroupId][ii++] !== undefined) {
  						cnt++;
  					}
  // 					this._trace_debug_if(cnt - 1 != iDiscCountAtChkIndex - iDiscCountAtBefore, "assertion failed: disc count exp = " + (cnt - 1) + ", but got " + (iDiscCountAtChkIndex - iDiscCountAtBefore) + " at key index = " + (chkIndex));
  				}
  			}
  		}
  * eslint-enable no-debugger *
  *end code block*/
  		// if any new requests have been created for reloading single-unit measures, execute and group them to get a single update event for them
  		var aReloadMeasureRequestId = [];
  		if (this.bReloadSingleUnitMeasures && aReloadMeasuresRequestDetails.length > 0) {
  			if (this.bUseBatchRequests) {
  				this.aBatchRequestQueue.push([AnalyticalBinding._requestType.reloadMeasuresQuery, aReloadMeasuresRequestDetails]);
  				Promise.resolve().then(AnalyticalBinding.prototype._processRequestQueue.bind(this));
  			} else {
  				for (var q = 0; q < aReloadMeasuresRequestDetails.length; q++){
  					var oReloadMeasuresRequestDetails2 = aReloadMeasuresRequestDetails[q];
  					this._executeQueryRequest(oReloadMeasuresRequestDetails2);
  				}
  			}
  
  			for (var p = 0; p < aReloadMeasuresRequestDetails.length; p++){
  				var oReloadMeasuresRequestDetails3 = aReloadMeasuresRequestDetails[p];
  				aReloadMeasureRequestId.push(oReloadMeasuresRequestDetails3.sRequestId);
  			}
  			this._considerRequestGrouping(aReloadMeasureRequestId);
  		}
  
  		// cleanup results entry array from added previous entry
  		if (aPreviousEntryServiceKey && aPreviousEntryServiceKey.length > 0) {
  			for (var r = 0, len2 = aPreviousEntryServiceKey.length; r < len2; r++) {
  				delete oData.results[r - len2];
  			}
  		}
  
  		// if unit check is required, then merge loaded data with already available data occuring directly after the new data
  		if (bUnitCheckRequired) {
  			iDiscardedEntriesCount += this._mergeLoadedKeyIndexWithSubsequentIndexes(oKeyIndexMapping, aAggregationLevel, aSelectedUnitPropertyName, oRequestDetails.bIsFlatListRequest);
  		}
  
  // 		this._trace_message("ReqExec", "", { servicelengthBefore: this.mServiceLength[sGroupId], lengthBefore: this.mLength[sGroupId], discardedEntriesCount: iDiscardedEntriesCount}, ["servicelengthBefore", "lengthBefore", "discardedEntriesCount"]);
  		// update group length
  		if (!oRequestDetails.bAvoidLengthUpdate) {
  			var bNewLengthSet = false;
  
  			if (oData.__count) {
  				this.mServiceLength[sGroupId] = parseInt(oData.__count, 10);
  				this.mLength[sGroupId] = this.mServiceLength[sGroupId] - iDiscardedEntriesCount;
  				this.mFinalLength[sGroupId] = true;
  
  				if (oRequestDetails.bIsFlatListRequest) {
  					this.iTotalSize = oData.__count;
  				}
  				bNewLengthSet = true;
  			}
  
  			// if we got data and the results + startindex is larger than the length we just apply this value to the length
  			if (this.mServiceLength[sGroupId] < iServiceStartIndex + iODataResultsLength) {
  				this.mServiceLength[sGroupId] = iServiceStartIndex + iODataResultsLength;
  				this.mLength[sGroupId] = iStartIndex + iODataResultsLength - iDiscardedEntriesCount;
  				this.mFinalLength[sGroupId] = false;
  			}
  
  			// if less entries are returned than have been requested set length accordingly
  			if (iODataResultsLength < iLength || iLength === undefined) {
  				this.mServiceLength[sGroupId] = iServiceStartIndex + iODataResultsLength;
  				this.mLength[sGroupId] = iStartIndex + oKeyIndexMapping.iIndex - iStartIndex;
  				this.mFinalLength[sGroupId] = true;
  				bNewLengthSet = true;
  			}
  
  			// check if there are any results at all
  			if (iODataResultsLength == 0) {
  				this.mLength[sGroupId] = this.mServiceLength[sGroupId] = 0;
  				this.mFinalLength[sGroupId] = true;
  				bNewLengthSet = true;
  			}
  
  			if (!bNewLengthSet && this.mLength[sGroupId] !== undefined && iDiscardedEntriesCount > 0) {
  				this.mLength[sGroupId] -= iDiscardedEntriesCount;
  			}
  		}
  // 		this._trace_message("ReqExec", "", { servicelengthAfter: this.mServiceLength[sGroupId], lengthAfter: this.mLength[sGroupId]}, ["servicelengthAfter", "lengthAfter"]);
  
  		this.bNeedsUpdate = true;
  
  		if (iDiscardedEntriesCount > 0) { // update load factor if entries have been discarded
  
  			// If all loaded entries have been discarded, we have the following situation:
  			// the last multi-unit entry was previously loaded with another data page, and thus it can happen, that all response entries
  			// will have to be discarded, since there already is a virtual multi-unit entry created.
  			// In this case we keep the last known load-factor stable.
  			if (oData.results.length - iDiscardedEntriesCount > 0) {
  				this.aMultiUnitLoadFactor[aAggregationLevel.length] = oData.results.length / (oData.results.length - iDiscardedEntriesCount);
  			}
  
  			if (this.aMultiUnitLoadFactor[aAggregationLevel.length] < 1.5) { // avoid too small factors
  				this.aMultiUnitLoadFactor[aAggregationLevel.length] = 2;
  			}
  		}
  		// #TH
  		oLogger.info("MultiUnit Situation in Group (" + sGroupId + "), discarded: " + iDiscardedEntriesCount + ", load-factor is now: " + this.aMultiUnitLoadFactor[aAggregationLevel.length]);
  // 		this._trace_debug_if(this.iMultiUnitLoadFactor < 1, "load factor cannot be lower than 1!");
  
  /* multi-unit verification: check length of loaded data with colected cumulated discarded counts */
  /*start code block*
  		this._checkLength(sGroupId, iStartIndex);
  *end code block*/
  
  // 		this._trace_leave("ReqExec", "_processGroupMembersQueryResponse", "", { lastLoadedIndex: oKeyIndexMapping.iIndex - 1, lastLoadedServiceIndex: oKeyIndexMapping.iServiceKeyIndex - 1, discardedEntriesCount: iDiscardedEntriesCount, multiUnitLoadFactor: this.aMultiUnitLoadFactor[aAggregationLevel.length] }, ["lastLoadedIndex","lastLoadedServiceIndex","discardedEntriesCount","multiUnitLoadFactor"]); // DISABLED FOR PRODUCTION
  	};
  
  /* multi-unit verification: check length of loaded data with colected cumulated discarded counts */
  /*start code block*
  * eslint-disable no-debugger *
  	AnalyticalBinding.prototype._checkLength = function(sGroupId, iStartIndex) {
  		var aKeyIndex = this.mKeyIndex[sGroupId];
  		var count = this.mServiceLength[sGroupId];
  		for (var i = iStartIndex, iMaxIndex = this.aDiscCount.length; i < iMaxIndex; i++) {
  			if (! this.aCheckDiscCount[i]) {
  				continue;
  			}
  			if (aKeyIndex[i] < 0 || aKeyIndex[i] == "ZERO") {
  				var aServiceKey = this._getServiceKeys(sGroupId, i);
  // 				this._trace_debug_if(!aServiceKey, "failed: no service keys found!?");
  				var iDiscCountAtChkIndex = this.aDiscCount[i];
  				var iDiscCountAtBefore = this.aDiscCount[i - 1] !== undefined ? this.aDiscCount[i - 1] : 0;
  // 				this._trace_debug_if(iDiscCountAtChkIndex - iDiscCountAtBefore != aServiceKey.length - 1, "mismatch: entry at pos " + (i) + " has " + (aServiceKey.length - 1) + " service keys, but discCount is " + (iDiscCountAtChkIndex - iDiscCountAtBefore));
  				count -= aServiceKey.length - 1; // useless..., because we do not check from 0, but from startIndex
  			}
  		}
  	};
  * eslint-enable no-debugger *
  *end code block*/
  
  	/**
  	 * @private
  	 */
  	AnalyticalBinding.prototype._processTotalSizeQueryResponse = function(oRequestDetails, oData) {
  		if (oData.__count == undefined) {
  			oLogger.fatal("missing entity count in query result");
  			return;
  		}
  		this.iTotalSize = oData.__count;
  	};
  
  	/**
  	 * @private
  	 */
  	AnalyticalBinding.prototype._processLevelMembersQueryResponse = function(oRequestDetails, oData) {
  		// local helper function to transform a block of entries in the level response to a response for a particular parent group
  		var that = this;
  		var sPreviousParentGroupId, aParentGroupODataResult;
  
  		var processSingleGroupFromLevelSubset = function (bProcessFirstLoadedGroup, bIncompleteGroupMembersSet) {
  			// transform the subset for processing as group members query response
  			var oGroupMembersRequestDetails = {
  				iRequestType : AnalyticalBinding._requestType.groupMembersQuery,
  				sRequestId : that._getRequestId(AnalyticalBinding._requestType.groupMembersQuery, {groupId: sPreviousParentGroupId}),
  				oAnalyticalQueryRequest : oRequestDetails.oAnalyticalQueryRequest,
  				sGroupId : sPreviousParentGroupId,
  				aSelectedUnitPropertyName : oRequestDetails.aSelectedUnitPropertyName,
  				aAggregationLevel : oRequestDetails.aAggregationLevel,
  				bIsFlatListRequest : oRequestDetails.bIsFlatListRequest,
  				bIsLeafGroupsRequest : oRequestDetails.bIsLeafGroupsRequest,
  				iStartIndex : bProcessFirstLoadedGroup ? oRequestDetails.iStartIndex : 0,
  				iLength : oRequestDetails.iLength,
  				bAvoidLengthUpdate : oRequestDetails.bAvoidLengthUpdate
  			}; // note that the keyIndexMapping is still missing; added later after handling of special cases
  
  			// special handling for the first group contained in this level load if it starts a new group
  			if (bProcessFirstLoadedGroup
  				&& oRequestDetails.iStartIndex > 0
  				&& (oRequestDetails.sGroupId_Missing_AtLevel != oGroupMembersRequestDetails.sGroupId
  						|| that._getKeys(oGroupMembersRequestDetails.sGroupId) === undefined)) {
  				// pendant to bIncompleteGroupMembersSet: set the finalLength of the previous group
  				var sParentGroupId = that._getParentGroupId(oGroupMembersRequestDetails.sGroupId);
  				var iPositionInParentGroup = that._findKeyIndex(sParentGroupId, that.mEntityKey[oGroupMembersRequestDetails.sGroupId]);
  				if (iPositionInParentGroup == -1) {
  					oLogger.fatal("assertion failed: failed to determine position of " + oGroupMembersRequestDetails.sGroupId + " in group " + sParentGroupId);
  				}
  				if (iPositionInParentGroup > 0 && that._getKey(sParentGroupId, iPositionInParentGroup - 1) !== undefined) {
  					var sPreviousGroupMemberKey = that._getKey(sParentGroupId, iPositionInParentGroup - 1);
  					var sPreviousGroupId = that._getGroupIdFromContext(that.oModel.getContext('/' + sPreviousGroupMemberKey),
  							that._getGroupIdLevel(oGroupMembersRequestDetails.sGroupId));
  					// only for development - if (that.mFinalLength[sPreviousGroupId]) oLogger.fatal("assertion failed that final length of previous group id is false");
  					// the final length of the previous must be set to true
  					that.mFinalLength[sPreviousGroupId] = true;
  					// and iStartIndex must be reset to 0, because a new group starts
  					oGroupMembersRequestDetails.iStartIndex = 0;
  				}
  			}
  			// special handling for the last group contained in this level load
  			if (bIncompleteGroupMembersSet) {
  				// this will force the next call of _processGroupMembersQueryResponse() below to maintain the partial length
  				oGroupMembersRequestDetails.iLength = aParentGroupODataResult.length;
  			}
  			oGroupMembersRequestDetails.oKeyIndexMapping = that._getKeyIndexMapping(oGroupMembersRequestDetails.sGroupId, oGroupMembersRequestDetails.iStartIndex);
  			var oParentGroupOData = jQuery.extend(true, {}, oData);
  			oParentGroupOData.results = aParentGroupODataResult;
  			that._processGroupMembersQueryResponse(oGroupMembersRequestDetails, oParentGroupOData);
  		};
  
  		// function implementation starts here
  
  		if (oData.results.length == 0) {
  			return;
  		}
  		// Collecting contexts
  		sPreviousParentGroupId = this._getGroupIdFromContext( // setup for loop
  				this.oModel.getContext("/" + this.oModel._getKey(oData.results[0])), oRequestDetails.iLevel - 1);
  		aParentGroupODataResult = [];
  		var bProcessFirstLoadedGroup = true;
  		for (var i = 0; i < oData.results.length; i++) {
  			// partition the result into several subsets each of which has a common parent group Id
  			var oEntry = oData.results[i];
  			var oContext = this.oModel.getContext("/" + this.oModel._getKey(oData.results[i]));
  			var sParentGroupId = this._getGroupIdFromContext(oContext, oRequestDetails.iLevel - 1);
  			if (sPreviousParentGroupId == sParentGroupId) {
  				aParentGroupODataResult.push(oEntry);
  				if (i < oData.results.length - 1) {
  					continue;
  				}
  			}
  			processSingleGroupFromLevelSubset(bProcessFirstLoadedGroup,
  											oData.results.length == oRequestDetails.iLength && i == oData.results.length - 1);
  			// setup for processing next parent group
  			bProcessFirstLoadedGroup = false;
  			if (sPreviousParentGroupId != sParentGroupId) {
  				aParentGroupODataResult = [ oEntry ];
  			}
  			sPreviousParentGroupId = sParentGroupId;
  		}
  		// process remaining left over (can happen if results contains more than one entry and group ID switches on last entry)
  		if (oData.results.length > 1 && aParentGroupODataResult.length == 1) {
  			processSingleGroupFromLevelSubset(bProcessFirstLoadedGroup, oData.results.length == oRequestDetails.iLength);
  		}
  	};
  
  
  	/**
  	 * @private
  	 */
  	AnalyticalBinding.prototype._processReloadMeasurePropertiesQueryResponse = function(oRequestDetails, oData) {
  		var oMultiUnitRepresentative = oRequestDetails.oMultiUnitRepresentative;
  		var sMultiUnitEntryKey = this.oModel.getKey(oMultiUnitRepresentative.oEntry);
  // 		this._trace_enter("ReqExec", "_processReloadMeasurePropertiesQueryResponse", "multi-unit key=" + sMultiUnitEntryKey); // DISABLED FOR PRODUCTION
  
  		if (oData.results.length != 1) {
  // 			this._trace_debug_if(true, "assertion failed: more than one entity for reloaded measure properties of entity with key " + sMultiUnitEntryKey);
  			oLogger.fatal("assertion failed: more than one entity for reloaded measure properties of entity with key " + sMultiUnitEntryKey);
  			return;
  		}
  
  		var oReloadedEntry = oData.results[0];
  		var oMultiUnitEntry = this.oModel.getObject("/" + sMultiUnitEntryKey);
  		if (!oMultiUnitEntry) {
  			oLogger.fatal("assertion failed: no entity found with key " + sMultiUnitEntryKey);
  			return;
  		}
  		var aMeasureName = oMultiUnitRepresentative.aReloadMeasurePropertyName;
  		for (var i = 0; i < aMeasureName.length; i++) {
  // 			this._trace_debug_if(oReloadedEntry[aMeasureName[i]] === undefined || oReloadedEntry[aMeasureName[i]] == "", "no value for reloaded measure property");
  			oMultiUnitEntry[aMeasureName[i]] = oReloadedEntry[aMeasureName[i]];
  		}
  // 		this._trace_leave("ReqExec", "_processReloadMeasurePropertiesQueryResponse", "measures=" + oMultiUnitRepresentative.aReloadMeasurePropertyName.join()); // DISABLED FOR PRODUCTION
  	};
  
  
  	/** *************************************************************** */
  
  	/**
  	 * @private
  	 */
  	AnalyticalBinding.prototype._getLoadedContextsForGroup = function(sGroupId, iStartIndex, iLength, bFetchAll) {
  		var aContext = [], oContext, i, fKey = this._getKeys(sGroupId), sKey;
  
  		if (!fKey) {
  			return aContext;
  		}
  
  		if (!iStartIndex) {
  			iStartIndex = 0;
  		}
  
  		if (!iLength) {
  			iLength = this.oModel.iSizeLimit;
  			//if the length is known, do not use the size limit of the model, but the known length
  			if (this.mFinalLength[sGroupId]) { // && this.mLength[sGroupId] < iLength) {
  				iLength = this.mLength[sGroupId];
  			}
  		}
  
  		if (bFetchAll) {
  			i = iStartIndex || 0;
  			sKey = fKey(i);
  			while (sKey) {
  				oContext = this.oModel.getContext('/' + sKey);
  				aContext.push(oContext);
  				i++;
  				sKey = fKey(i);
  			}
  			return aContext;
  		}
  
  		// Loop through known data and check whether we already have all rows loaded
  		for (i = iStartIndex; i < iStartIndex + iLength; i++) {
  			sKey = fKey(i);
  			if (!sKey) {
  				break;
  			}
  			oContext = this.oModel.getContext('/' + sKey);
  			aContext.push(oContext);
  		}
  
  		return aContext;
  	};
  
  	/**
  	 * @private
  	 */
  	AnalyticalBinding.prototype._calculateRequiredGroupSection = function(sGroupId, iStartIndex, iLength, iThreshold, aContext) {
  		// implementation copied from ODataListBinding; name changed here, because analytical binding comprises more calculations
  		var iSectionLength, iSectionStartIndex, iPreloadedSubsequentIndex, iPreloadedPreviousIndex, iRemainingEntries, oSection = {}, fKey = this._getKeys(sGroupId), sKey;
  
  		iSectionStartIndex = iStartIndex;
  		iSectionLength = 0;
  
  		// check which data exists before startindex; If all necessary data is loaded iPreloadedPreviousIndex stays undefined
  		if (!fKey) {
  			iPreloadedPreviousIndex = iStartIndex;
  			iPreloadedSubsequentIndex = iStartIndex + iLength;
  		} else {
  			for (var i = iStartIndex - 1; i >= Math.max(iStartIndex - iThreshold, 0); i--) {
  				sKey = fKey(i);
  				if (!sKey) {
  					iPreloadedPreviousIndex = i + 1;
  					break;
  				}
  			}
  			// check which data is already loaded after startindex; If all necessary data is loaded iPreloadedSubsequentIndex stays undefined
  			for (var j = iStartIndex + iLength; j < iStartIndex + iLength + iThreshold; j++) {
  				sKey = fKey(j);
  				if (!sKey) {
  					iPreloadedSubsequentIndex = j;
  					break;
  				}
  			}
  		}
  		// calculate previous remaining entries
  		iRemainingEntries = iStartIndex - iPreloadedPreviousIndex;
  		if (iPreloadedPreviousIndex && iStartIndex > iThreshold && iRemainingEntries < iThreshold) {
  			if (aContext.length !== iLength) {
  				iSectionStartIndex = iStartIndex - iThreshold;
  			} else {
  				iSectionStartIndex = iPreloadedPreviousIndex - iThreshold;
  			}
  
  			iSectionLength = iThreshold;
  		}
  
  		// prevent startIndex from getting out of bounds
  		// FIX for BCP(1570041982)
  		// If the startIndex is negative, the $skip value will also be negative, and the length might also be bigger than necessary
  		iSectionStartIndex = Math.max(iSectionStartIndex, 0);
  
  		// No negative preload needed; move startindex if we already have some data
  		if (iSectionStartIndex === iStartIndex) {
  			iSectionStartIndex += aContext.length;
  		}
  
  		//read the rest of the requested data
  		if (aContext.length !== iLength) {
  			iSectionLength += iLength - aContext.length;
  		}
  
  		//calculate subsequent remaining entries
  		iRemainingEntries = iPreloadedSubsequentIndex - iStartIndex - iLength;
  
  		if (iRemainingEntries == 0) {
  			iSectionLength += iThreshold;
  		}
  
  		if (iPreloadedSubsequentIndex && iRemainingEntries < iThreshold && iRemainingEntries > 0) {
  			//check if we need to load previous entries; If not we can move the startindex
  			// changed the ">=" to ">", because a fix was not migrated, see commit #455622
  			// FIX for BCP(1570041982)
  			if (iSectionStartIndex > iStartIndex) {
  				iSectionStartIndex = iPreloadedSubsequentIndex;
  				iSectionLength += iThreshold;
  			}
  
  		}
  
  		//check final length and adapt sectionLength if needed.
  		if (this.mFinalLength[sGroupId] && this.mLength[sGroupId] < (iSectionLength + iSectionStartIndex)) {
  			iSectionLength = this.mLength[sGroupId] - iSectionStartIndex;
  		}
  
  		oSection.startIndex = iSectionStartIndex;
  		oSection.length = iSectionLength;
  
  		// this._trace(sGroupId, "calculateSection:\tstart = " + iSectionStartIndex + "\tlength = " + iSectionLength); // DISABLED FOR PRODUCTION
  
  		return oSection;
  	};
  
  	/**
  	 * Searches for missing members in the sub groups and subsequent siblings and ancestors of the given sGroupId
  	 * @returns {Object} Either { groupId_Missing, startIndex_Missing, length_Missing }
  	 * expressing the number (length_Missing) of missing contexts starting in group (groupId_Missing)
  	 * at position (startIndex_Missing) using depth-first traversal of loaded data,
  	 * or { null, length_Missing } if all groups starting with the given ID (sGroupId) and all subsequent are
  	 * completely loaded and still (length_Missing) further members are missing, which cannot be fulfilled by loading data.
  	 * Special case: { null, 0 } denotes that everything is loaded for the requested length.
  	 * @private
  	 */
  	AnalyticalBinding.prototype._calculateRequiredGroupExpansion = function(sGroupId, iAutoExpandGroupsToLevel, iStartIndex, iLength) {
  		var oNO_MISSING_MEMBER = { groupId_Missing: null, length_Missing: 0 };
  		var that = this;
  
  		/**
  		 * helper function
  		 * 		Searches for missing members in the sub groups of the given sGroupId
  		 * @returns {Object} Either { groupId_Missing, startIndex_Missing, length_Missing }
  		 * expressing the number (length_Missing) of missing contexts starting in group (groupId_Missing)
  		 * at position (startIndex_Missing) using depth-first traversal of loaded data,
  		 * or { null, length_Missing } if the group with given ID (sGroupId) is completely loaded
  		 * and still (length_Missing) further members (of other groups) are missing.
  		 * Special case: { null, 0 } denotes that everything is loaded.
  		 */
  		var calculateRequiredSubGroupExpansion = function(sGroupId, iAutoExpandGroupsToLevel, iStartIndex, iLength) {
  			var iLevel = that._getGroupIdLevel(sGroupId);
  			if (iLevel == iAutoExpandGroupsToLevel) {
  				var aContext = that._getLoadedContextsForGroup(sGroupId, iStartIndex, iLength);
  				var iLastLoadedIndex = iStartIndex + aContext.length - 1;
  
  				if (aContext.length >= iLength) {
  					return oNO_MISSING_MEMBER;
  				} else if (that.mFinalLength[sGroupId]) {
  					if (aContext.length >= that.mLength[sGroupId]) {
  						return { groupId_Missing: null, length_Missing: iLength - aContext.length }; // group completely loaded, but some members are still missing
  					} else {
  						return { groupId_Missing: sGroupId, startIndex_Missing: iLastLoadedIndex + 1, length_Missing: iLength - aContext.length }; // loading must start here
  					}
  				} else {
  					return { groupId_Missing: sGroupId, startIndex_Missing: iLastLoadedIndex + 1, length_Missing: iLength - aContext.length }; // loading must start here
  				}
  			}
  			// deepest expansion level not yet reached, so traverse groups in depth-first order
  			var aContext2 = that._getLoadedContextsForGroup(sGroupId, iStartIndex, iLength);
  			var iLength_Missing = iLength, iLastLoadedIndex2 = iStartIndex + aContext2.length - 1;
  			for (var i = -1, oContext; (oContext = aContext2[++i]) !== undefined; ) {
  				iLength_Missing--; // count the current context
  				var oGroupExpansionFirstMember = calculateRequiredSubGroupExpansion(that._getGroupIdFromContext(oContext, iLevel + 1), iAutoExpandGroupsToLevel, 0, iLength_Missing);
  				if (oGroupExpansionFirstMember.groupId_Missing == null) {
  					if (oGroupExpansionFirstMember.length_Missing == 0) {
  						return oGroupExpansionFirstMember; // finished - everything is loaded
  					} else {
  						iLength_Missing = oGroupExpansionFirstMember.length_Missing;
  					}
  				} else {
  					return oGroupExpansionFirstMember; // loading must start here
  				}
  				if (iLength_Missing == 0) {
  					break;
  				}
  			}
  
  			if (that.mFinalLength[sGroupId] || iLength_Missing == 0) {
  				return { groupId_Missing: null, length_Missing: iLength_Missing }; // group completely loaded; maybe some members are still missing
  			} else {
  				return { groupId_Missing: sGroupId, startIndex_Missing: iLastLoadedIndex2 + 1, length_Missing: iLength_Missing }; // loading must start here
  			}
  		};
  
  		// function implementation starts here
  		var iLevel = this._getGroupIdLevel(sGroupId);
  		if (iLevel == iAutoExpandGroupsToLevel + 1) {
  			sGroupId = this._getParentGroupId(sGroupId);
  			--iLevel;
  		}
  		if (sGroupId == null || iLevel > iAutoExpandGroupsToLevel) {
  			return oNO_MISSING_MEMBER;
  		}
  
  		var iLength_Missing = iLength, iCurrentStartIndex = iStartIndex;
  		while (sGroupId != null) {
  			var oGroupExpansionFirstMember = calculateRequiredSubGroupExpansion(sGroupId, iAutoExpandGroupsToLevel, iCurrentStartIndex, iLength_Missing);
  			if (oGroupExpansionFirstMember.groupId_Missing != null) {
  				return oGroupExpansionFirstMember;
  			} else if (oGroupExpansionFirstMember.length_Missing == 0) {
  				return oGroupExpansionFirstMember;
  			} else { // last sub-tree is complete, so continue calculation w/ next sibling
  				var bFoundSibling = false;
  				while (!bFoundSibling) {
  					var sParentGroupId = this._getParentGroupId(sGroupId);
  					if (sParentGroupId == null) {
  						sGroupId = sParentGroupId;
  						--iLevel;
  						break;
  					}
  					// determine position of sGroupId in members of group w/ ID sParentGroupId
  					var sGroupKey = this.mEntityKey[sGroupId];
  					if (!sGroupKey) {
  						//oLogger.fatal("assertion failed: entitykey for group w/ ID " + sGroupId + " not available");
  						return oNO_MISSING_MEMBER;
  					}
  					var iGroupIndex = this._findKeyIndex(sParentGroupId,sGroupKey);
  					if (iGroupIndex == -1) {
  						//oLogger.fatal("assertion failed: group w/ ID " + sGroupId + " not found in members of parent w/ ID " + sParentGroupId);
  						return oNO_MISSING_MEMBER;
  					}
  					if (iGroupIndex == this._getKeyCount(sParentGroupId) - 1) {
  						if (this.mFinalLength[sParentGroupId]) { // last member in group
  							sGroupId = sParentGroupId;
  							--iLevel;
  							continue; // continue with next sibling one level up
  						} else { // some members of this level have not been loaded yet --> loading must continue at this point
  							return { groupId_Missing: sParentGroupId, startIndex_Missing: iGroupIndex + 1, length_Missing: iLength_Missing };
  						}
  					} else { // continue with next sibling in same level
  						sGroupKey = this._getKey(sParentGroupId, iGroupIndex + 1);
  						sGroupId = this._getGroupIdFromContext(this.oModel.getContext('/' + sGroupKey), iLevel);
  						bFoundSibling = true;
  					}
  				}
  				iCurrentStartIndex = 0;
  				iLength_Missing = oGroupExpansionFirstMember.length_Missing;
  			}
  		}
  		return { groupId_Missing: null, length_Missing: iLength_Missing }; // all data loaded; number of requested members cannot be fulfilled
  	};
  
  	/**
  	 * @private
  	 */
  	AnalyticalBinding.prototype._getResourcePath = function() {
  		return this.isRelative() ? this.oModel.resolve(this.sPath, this.getContext()) : this.sPath;
  	};
  
  	/**
  	 * @private
  	 */
  	AnalyticalBinding.prototype._getEntitySet = function() {
  		var sEntitySet = this.sEntitySetName;
  
  		if (!sEntitySet) {
  			// assume absolute path complying with conventions from OData4SAP spec
  			sEntitySet = this.sPath.split("/")[1];
  
  			if (sEntitySet.indexOf("(") != -1) {
  				sEntitySet = sEntitySet.split("(")[0] + "Results";
  			}
  		}
  		return sEntitySet;
  
  	};
  
  	/**
  	 * get the effective sort order for a given property considering the column settings, local sort() calls and a global sort order from bindRows
  	 * @private
  	 */
  	AnalyticalBinding.prototype._getEffectiveSortOrder = function(sPropertyName) {
  		for (var i = 0; i < this.aSorter.length; i++) {
  			if (this.aSorter[i] && this.aSorter[i].sPath == sPropertyName) {
  				return this.aSorter[i].bDescending ? odata4analytics.SortOrder.Descending : odata4analytics.SortOrder.Ascending;
  			}
  		}
  		return null;
  	};
  
  
  	/**
  	 * get the filter operator that matches the sort order set for the given property
  	 * @private
  	 */
  	AnalyticalBinding.prototype._getFilterOperatorMatchingPropertySortOrder = function(sPropertyName, bWithEqual) {
  		var sFilterOperator;
  		switch (this._getEffectiveSortOrder(sPropertyName)) {
  			case odata4analytics.SortOrder.Ascending:
  				if (bWithEqual) {
  					sFilterOperator = FilterOperator.GE;
  				} else {
  					sFilterOperator = FilterOperator.GT;
  				}
  				break;
  			case odata4analytics.SortOrder.Descending:
  				if (bWithEqual) {
  					sFilterOperator = FilterOperator.LE;
  				} else {
  					sFilterOperator = FilterOperator.LT;
  				}
  				break;
  			default: // null
  				 // default if no sort order applied - matches the default ascending order set for grouped dimensions in prepare...QueryRequest()
  				sFilterOperator = FilterOperator.GT;
  		}
  		return sFilterOperator;
  	};
  
  	/**
  	 * @private
  	 */
  	AnalyticalBinding.prototype._convertDeprecatedFilterObjects = function(aFilter) {
  		if (!aFilter) {
  			return aFilter;
  		}
  
  		// check if some filter object use the deprecated class sap.ui.model.odata.Filter;
  		// if so, convert them to sap.ui.model.Filter
  		var ODataFilter = sap.ui.require("sap/ui/model/odata/Filter");
  		if ( typeof ODataFilter === 'function' ) {
  			for (var i = 0, l = aFilter.length; i < l; i++) {
  				if (aFilter[i] instanceof ODataFilter) {
  					aFilter[i] = aFilter[i].convert();
  				}
  			}
  		}
  		return aFilter;
  	};
  
  	/********************************
  	 *** Processing Group IDs
  	 ********************************/
  
  	/**
  	 * @private
  	 */
  	AnalyticalBinding.prototype._getGroupIdFromContext = function(oContext, iLevel) {
  
  		if (!oContext) {
  			return null;
  		}
  		var sGroupId = "/";
  		var sDimensionMember = null;
  		if (iLevel > this.aAggregationLevel.length) {
  			oLogger.fatal("assertion failed: aggregation level deeper than number of current aggregation levels");
  		}
  		for (var i = 0; i < iLevel; i++) {
  			sDimensionMember = oContext.getProperty(this.aAggregationLevel[i]);
  			if (sDimensionMember != null) {
  				sGroupId += encodeURIComponent(sDimensionMember) + "/"; // encode to escape slashes and at signs in the value
  			} else {
  				sGroupId += "@/";
  			}
  		}
  
  		return sGroupId;
  	};
  
  	/**
  	 * @private
  	 */
  	AnalyticalBinding.prototype._getGroupIdLevel = function(sGroupId) {
  		if (sGroupId == null) {
  			oLogger.fatal("assertion failed: no need to determine level of group ID = null");
  			return -1;
  		}
  		return sGroupId.split("/").length - 2;
  	};
  
  	/**
  	 * @private
  	 */
  	AnalyticalBinding.prototype._getGroupIdComponents = function(sGroupId) {
  		if (sGroupId == null) {
  			return null;
  		}
  		var aGroupId = sGroupId.split("/");
  		var aDecodedComponent = [];
  		for (var i = 1; i < aGroupId.length - 1; i++) { // skip leading and trailing "" array elements
  			if (aGroupId[i] == "@") {
  				aDecodedComponent[i - 1] = null;
  			} else {
  				aDecodedComponent[i - 1] = decodeURIComponent(aGroupId[i]);
  			}
  		}
  		return aDecodedComponent;
  	};
  
  	/**
  	 * @param {int} iNumLevels anchestors starting at the root if greater than 0, or starting at the parent of sGroupId if less than 0.
  	 * @private
  	 */
  	AnalyticalBinding.prototype._getGroupIdAncestors = function(sGroupId, iNumLevels) {
  		if (!iNumLevels) {
  			return [];
  		}
  		if (sGroupId == null) {
  			oLogger.fatal("group ID null does not have ancestors");
  			return [];
  		}
  		if (sGroupId == "/") {
  			if (Math.abs(iNumLevels) == 1) {
  				return [ null ];
  			} else {
  				oLogger.fatal("invalid level count " + iNumLevels + " for ancestors of groupId " + sGroupId);
  				return [];
  			}
  		}
  		var aGroupId = sGroupId.split("/");
  		var aAncestorGroupId = [], sAncestorGroupId = "";
  		var iFromLevel = 0, iToLevel = aGroupId.length - 3;
  		if (iNumLevels > 0) {
  			if (iNumLevels - 1 > iToLevel) {
  				oLogger.fatal("invalid level count " + iNumLevels + " for ancestors of groupId " + sGroupId);
  			} else {
  				iToLevel = iNumLevels - 1;
  			}
  		} else if (-(iNumLevels + 1) > iToLevel) {
  			oLogger.fatal("invalid level count " + iNumLevels + " for ancestors of groupId " + sGroupId);
  		} else {
  			iFromLevel = iToLevel + 1 + iNumLevels;
  			for (var i = 0; i < iFromLevel; i++) {
  				sAncestorGroupId += aGroupId[i] + "/";
  			}
  		}
  		for (var j = iFromLevel; j <= iToLevel; j++) {
  			sAncestorGroupId += aGroupId[j] + "/";
  			aAncestorGroupId.push(sAncestorGroupId);
  		}
  		return aAncestorGroupId;
  	};
  
  	/**
  	 * @private
  	 */
  	AnalyticalBinding.prototype._getParentGroupId = function(sGroupId) {
  		return this._getGroupIdAncestors(sGroupId, -1)[0];
  	};
  
  	AnalyticalBinding.prototype._removeDuplicatesFromStringArray = function(aString) {
  		var oTemp = {};
  		for (var i = 0; i < aString.length; i++) {
  			oTemp[aString[i]] = true;
  		}
  		var aUniqueString = [];
  		for (var s in oTemp) {
  			aUniqueString.push(s);
  		}
  		return aUniqueString;
  	};
  
  
  	/********************************
  	 *** Maintaining handles of pending requests
  	 ********************************/
  
  	/**
  	 * Get an ID for a new request handle yet to be registered
  	 *
  	 * @private
  	 */
  	AnalyticalBinding.prototype._getIdForNewRequestHandle = function() {
  		if (this.oPendingRequestHandle === undefined) {
  			this.oPendingRequestHandle = [];
  		}
  		// find first unused slot or extend array
  		for (var i = 0; i < this.oPendingRequestHandle.length; i++) {
  			if (this.oPendingRequestHandle[i] === undefined) {
  				return i;
  			}
  		}
  		this.oPendingRequestHandle[this.oPendingRequestHandle.length] = undefined;
  		return this.oPendingRequestHandle.length - 1;
  	};
  
  	/**
  	 * Register a new request handle with its given request ID
  	 *
  	 * @private
  	 */
  	AnalyticalBinding.prototype._registerNewRequestHandle = function(iRequestHandleId, oRequestHandle) {
  		if (this.oPendingRequestHandle[iRequestHandleId] !== undefined) {
  			oLogger.fatal("request handle ID already in use");
  		}
  		this.oPendingRequestHandle[iRequestHandleId] = oRequestHandle;
  	};
  
  	/**
  	 * Deregister handle of completed request
  	 *
  	 * @private
  	 */
  	AnalyticalBinding.prototype._deregisterHandleOfCompletedRequest = function(iRequestHandleId) {
  		if (jQuery.isEmptyObject(this.oPendingRequestHandle)) {
  			oLogger.warning("No request handles to be cleared. Previous abort/resetData?");
  			return;
  		}
  		if (this.oPendingRequestHandle[iRequestHandleId] === undefined) {
  			oLogger.fatal("no handle found for this request ID");
  		}
  		this.oPendingRequestHandle[iRequestHandleId] = undefined;
  	};
  
  	/**
  	 * Abort all currently sent requests, which have not yet been completed
  	 *
  	 * @private
  	 */
  	AnalyticalBinding.prototype._abortAllPendingRequestsByHandle = function() {
  // 		this._trace_enter("ReqHandle", "_abortAllPendingRequestsByHandle"); // DISABLED FOR PRODUCTION
  		for (var i = 0; i < this.oPendingRequestHandle.length; i++) {
  			if (this.oPendingRequestHandle[i]) {
  // 				this._trace_message("ReqHandle", "abort index " + i);
  				if (this.oPendingRequestHandle[i] !== undefined) {
  					this.oPendingRequestHandle[i].abort();
  				}
  			}
  		}
  		this.oPendingRequestHandle = [];
  // 		this._trace_leave("ReqHandle", "_abortAllPendingRequestsByHandle"); // DISABLED FOR PRODUCTION
  	};
  
  	/********************************
  	 *** Maintaining pending requests
  	 ********************************/
  
  	/**
  	 * Construct a request ID for a query request of the specified type
  	 *
  	 * @private
  	 */
  	AnalyticalBinding.prototype._getRequestId = function(iRequestType, mParameters) {
  		switch (iRequestType) {
  		case AnalyticalBinding._requestType.groupMembersQuery:
  			if (mParameters.groupId === undefined) {
  				oLogger.fatal("missing group ID");
  			}
  			return AnalyticalBinding._requestType.groupMembersQuery + (mParameters.groupId == null ? "" : mParameters.groupId);
  		case AnalyticalBinding._requestType.levelMembersQuery:
  			if (mParameters.level === undefined) {
  				oLogger.fatal("missing level");
  			}
  			if (mParameters.groupId === undefined) {
  				oLogger.fatal("missing groupId");
  			}
  			// for accelerated auto-expand, group Id does not provide context, i.e. filter condition, for the requested data, but is only a starting point
  			return "" + AnalyticalBinding._requestType.levelMembersQuery + mParameters.level + (mParameters.tupleIndex ? "-" + mParameters.tupleIndex : "");
  		case AnalyticalBinding._requestType.totalSizeQuery:
  			return AnalyticalBinding._requestType.totalSizeQuery;
  		case AnalyticalBinding._requestType.reloadMeasuresQuery:
  			if (!mParameters.multiUnitEntryKey) {
  				oLogger.fatal("missing multi unit entry key");
  			}
  			return AnalyticalBinding._requestType.reloadMeasuresQuery + mParameters.multiUnitEntryKey;
  		default:
  			oLogger.fatal("invalid request type " + iRequestType);
  			return -1;
  		}
  	};
  
  	/**
  	 * Register another request to maintain its lifecycle (pending, completed)
  	 *
  	 * @private
  	 */
  	AnalyticalBinding.prototype._registerNewRequest = function(sRequestId) {
  		if (sRequestId == undefined || sRequestId == "") {
  			oLogger.fatal("missing request ID");
  			return;
  		}
  		if (!this.oPendingRequests[sRequestId]) {
  			this.oPendingRequests[sRequestId] = 1;
  		} else {
  			++this.oPendingRequests[sRequestId];
  		}
  	};
  
  	/**
  	 * Declare a group of related (pending) requests
  	 *
  	 * @private
  	 */
  	AnalyticalBinding.prototype._considerRequestGrouping = function(aRequestId) {
  		for (var i = -1, sRequestId; (sRequestId = aRequestId[++i]) !== undefined; ) {
  			if (this.oGroupedRequests[sRequestId] === undefined) {
  				this.oGroupedRequests[sRequestId] = {};
  			}
  			var oGroup = this.oGroupedRequests[sRequestId];
  			for (var j = 0; j < aRequestId.length; j++) {
  				oGroup[aRequestId[j]] = true;
  			}
  		}
  	};
  
  	/**
  	 * Is a request pending for a given group ID?
  	 *
  	 * @private
  	 */
  	AnalyticalBinding.prototype._isRequestPending = function(sRequestId) {
  		return this.oPendingRequests[sRequestId] != undefined && this.oPendingRequests[sRequestId] > 0;
  	};
  
  	/**
  	 * Deregister a request, because its data have been received and processed. A call to this method must be followed
  	 * (not immediately, but logically) by this._cleanupGroupingForCompletedRequest to cleanup grouping information.
  	 *
  	 * @private
  	 */
  	AnalyticalBinding.prototype._deregisterCompletedRequest = function(sRequestId) {
  		// in case there are no pending request, log a warning. This might happen during a refresh call
  		// helps to keep track of timing issues / race conditions with already returned requests
  		if (jQuery.isEmptyObject(this.oPendingRequests)) {
  			oLogger.warning("There are no pending requests which could be set to 'completed'.");
  			return;
  		}
  
  		if (!this.oPendingRequests[sRequestId]) {
  			oLogger.fatal("assertion failed: there is no pending request ID " + sRequestId);
  		}
  		if (this.oPendingRequests[sRequestId] == 1) {
  			delete this.oPendingRequests[sRequestId];
  		} else {
  			--this.oPendingRequests[sRequestId];
  		}
  	};
  
  	/**
  	 * Cleanup request grouping, because its data have been received and processed. This method allows a caller to determine if it is possible
  	 * to raise the "all data received" event for a group of related OData requests.
  	 *
  	 * A call to this method must be preceded by this._deregisterCompletedRequest to mark the received response.
  	 *
  	 * @return {boolean} whether or not all requests grouped together with this request have now been completed
  	 * @private
  	 */
  	AnalyticalBinding.prototype._cleanupGroupingForCompletedRequest = function(sRequestId) {
  		if (this._isRequestPending(sRequestId)) {
  			return false;
  		}
  		var bGroupCompleted = true;
  		if (this.oGroupedRequests[sRequestId] != undefined) {
  			for ( var sOtherRequestId in this.oGroupedRequests[sRequestId]) {
  				if (this.oPendingRequests[sOtherRequestId]) {
  					bGroupCompleted = false;
  					break;
  				}
  			}
  		}
  		if (bGroupCompleted) {
  			var oRelatedGroup = this.oGroupedRequests[sRequestId];
  			delete this.oGroupedRequests[sRequestId];
  			for ( var sOtherRequestId2 in oRelatedGroup) {
  				if (sOtherRequestId2 != sRequestId) {
  					this._cleanupGroupingForCompletedRequest(sOtherRequestId2);
  				}
  			}
  		}
  		return bGroupCompleted;
  	};
  
  	//********************************************************************************
  	//*** Service data consolidation (for multi-unit entities)
  	//********************************************************************************/
  
  	AnalyticalBinding.prototype._getKeyIndexMapping = function(sGroupId, iStartIndex) {
  		var aKeyIndex = this.mKeyIndex[sGroupId];
  		var aServiceKey = this.mServiceKey[sGroupId];
  		var iServiceKeyIndex = iStartIndex;
  		if (aKeyIndex !== undefined) { // find appropriate service key index for given start index
  			// search for the last occupied key index
  			var iLastOccupiedIndex = iStartIndex;
  			if (iLastOccupiedIndex > 0) {
  				while (--iLastOccupiedIndex > 0) {
  					if (aKeyIndex[iLastOccupiedIndex] !== undefined) {
  						break;
  					}
  				}
  			}
  			var iLastOccupiedServiceKeyIndex;
  			if (iLastOccupiedIndex == 0) {
  				iLastOccupiedServiceKeyIndex = 0;
  			} else {
  				if (aKeyIndex[iLastOccupiedIndex] >= 0) {
  					iLastOccupiedServiceKeyIndex = aKeyIndex[iLastOccupiedIndex];
  				} else if (aKeyIndex[iLastOccupiedIndex + 1] === undefined) { // iLastOccupiedIndex is the last key index before hole.
  					iLastOccupiedServiceKeyIndex = aKeyIndex[iLastOccupiedIndex] == "ZERO" ? 0 : -aKeyIndex[iLastOccupiedIndex];
  					while (aServiceKey[iLastOccupiedServiceKeyIndex + 1] !== undefined) {
  						++iLastOccupiedServiceKeyIndex;
  					}
  				} else { // iLastOccupiedServiceKeyIndex is the service key index before start of service keys related to next key index.
  					iLastOccupiedServiceKeyIndex = Math.abs(aKeyIndex[iLastOccupiedIndex + 1]) - 1;
  				}
  				if (aServiceKey[iLastOccupiedServiceKeyIndex] === undefined) {
  					oLogger.fatal("assertion failed: no service key at iLastOccupiedServiceKeyIndex = " + iLastOccupiedServiceKeyIndex);
  				}
  			}
  			var iDistance = iStartIndex - iLastOccupiedIndex;
  			iServiceKeyIndex = iLastOccupiedServiceKeyIndex + iDistance;
  		}
  		var oKeyIndexMapping = {
  				sGroupId : sGroupId,
  				iIndex : iStartIndex,
  				iServiceKeyIndex : iServiceKeyIndex
  		};
  		return oKeyIndexMapping;
  	};
  
  	AnalyticalBinding.prototype._moveKeyIndexMapping = function(oKeyIndexMapping, iIndexOffset) {
  		return this._getKeyIndexMapping(oKeyIndexMapping.sGroupId, oKeyIndexMapping.iIndex + iIndexOffset);
  	};
  
  	// access entry key for a given group ID and index
  	AnalyticalBinding.prototype._getKey = function(sGroupId, iIndex) { // replaces this.mKey[sGroupId][i] in Table.js
  		var iServiceKeyIndex = this.mKeyIndex[sGroupId][iIndex];
  		if (iServiceKeyIndex === undefined) {
  			return undefined;
  		}
  		if (iServiceKeyIndex >= 0) {
  			return this.mServiceKey[sGroupId][iServiceKeyIndex];
  		}
  
  		if (this.mMultiUnitKey[sGroupId] === undefined) {
  			oLogger.fatal("assertion failed: missing expected multi currency key for group with ID " + sGroupId);
  // 			this._trace_debug_if(true, "assertion failed: missing expected multi currency key for group with ID " + sGroupId);
  			return null;
  		}
  		var sKey = this.mMultiUnitKey[sGroupId][iIndex];
  		if (sKey === undefined) {
  			oLogger.fatal("assertion failed: missing expected multi currency key for group with ID " + sGroupId + " at pos " + iIndex);
  // 			this._trace_debug_if(true, "assertion failed: missing expected multi currency key for group with ID " + sGroupId + " at pos " + iIndex);
  			return null;
  		}
  		return sKey;
  	};
  
  	// access entry key array for a given group ID (as function)
  	AnalyticalBinding.prototype._getKeys = function(sGroupId) { // replaces this.mKey[sGroupId][i] in Table.js
  		if (this.mKeyIndex[sGroupId] === undefined) {
  			return undefined;
  		}
  		var that = this;
  		return function (iIndex) {
  			return that._getKey(sGroupId, iIndex);
  		};
  	};
  
  	// access array of service entry keys for a given group ID and index
  	AnalyticalBinding.prototype._getServiceKeys = function(sGroupId, iIndex) {
  		var aKeyIndex = this.mKeyIndex[sGroupId];
  		if (aKeyIndex === undefined) {
  			return undefined;
  		}
  		var aServiceKey = this.mServiceKey[sGroupId],
  			iServiceKeyIndex = aKeyIndex[iIndex];
  
  		if (iServiceKeyIndex === undefined) {
  			return undefined;
  		}
  		if (iServiceKeyIndex >= 0) {
  			return [ aServiceKey[iServiceKeyIndex] ];
  		}
  
  		var aGroupIndexServiceKey = [];
  		if (aKeyIndex[iIndex + 1] === undefined) {
  			iServiceKeyIndex = aKeyIndex[iIndex] == "ZERO" ? 0 : -aKeyIndex[iIndex];
  			while (aServiceKey[iServiceKeyIndex] !== undefined) {
  				aGroupIndexServiceKey.push(aServiceKey[iServiceKeyIndex++]);
  			}
  		} else {
  			iServiceKeyIndex = aKeyIndex[iIndex] == "ZERO" ? 0 : -aKeyIndex[iIndex];
  			for (var i = iServiceKeyIndex, iNextEntryIndex = Math.abs(aKeyIndex[iIndex + 1]); i < iNextEntryIndex; i++) {
  				aGroupIndexServiceKey.push(aServiceKey[i]);
  			}
  		}
  		return aGroupIndexServiceKey;
  	};
  
  	// number of keys in a group
  	AnalyticalBinding.prototype._getKeyCount = function(sGroupId) { // replaces this.mKey[sGroupId].length in Table.js
  		if (this.mKeyIndex[sGroupId] === undefined) {
  			return undefined;
  		}
  		return this.mKeyIndex[sGroupId].length;
  	};
  
  	// substitute for indexOf in the key array for some group
  	AnalyticalBinding.prototype._findKeyIndex = function(sGroupId, sKey) {
  		// TODO optimize by first looking into mMultiUnitKey; if not found, search in mServiceKey; if not found, return -1
  
  		// naive implementation follows
  		var aKeyIndex = this.mKeyIndex[sGroupId];
  		var aServiceKey = this.mServiceKey[sGroupId];
  		var aMultiUnitKey = this.mMultiUnitKey[sGroupId];
  		for (var i = 0; i < this.mLength[sGroupId]; i++) {
  			if (aKeyIndex[i] < 0) {
  				if (aMultiUnitKey[i] == sKey) {
  					return i;
  				}
  			} else if (aServiceKey[aKeyIndex[i]] == sKey) {
  				return i;
  			}
  		}
  		return -1;
  	};
  
  	// save the key of a loaded entry at the given indexes
  	AnalyticalBinding.prototype._setServiceKey = function(oKeyIndexMapping, sServiceKey) {
  		if (!this.mServiceKey[oKeyIndexMapping.sGroupId]) {
  			this.mServiceKey[oKeyIndexMapping.sGroupId] = [];
  		}
  		if (!this.mKeyIndex[oKeyIndexMapping.sGroupId]) {
  			this.mKeyIndex[oKeyIndexMapping.sGroupId] = [];
  		}
  
  		var bNewKey = this.mServiceKey[oKeyIndexMapping.sGroupId][oKeyIndexMapping.iServiceKeyIndex] === undefined;
  
  		this.mServiceKey[oKeyIndexMapping.sGroupId][oKeyIndexMapping.iServiceKeyIndex++] = sServiceKey;
  		this.mKeyIndex[oKeyIndexMapping.sGroupId][oKeyIndexMapping.iIndex++] = oKeyIndexMapping.iServiceKeyIndex - 1;
  
  		return bNewKey;
  	};
  
  	// save the keys of adjacent identical entries only differing in the given units; oKeyIndexMapping points to the position after the first multi-unit entry
  	// returns number of service keys that were not yet available locally
  	AnalyticalBinding.prototype._setAdjacentMultiUnitKeys = function(oKeyIndexMapping, oMultiUnitRepresentative, aMultiUnitEntry) {
  		if (!this.mServiceKey[oKeyIndexMapping.sGroupId]) {
  			this.mServiceKey[oKeyIndexMapping.sGroupId] = [];
  		}
  		if (!this.mKeyIndex[oKeyIndexMapping.sGroupId]) {
  			this.mKeyIndex[oKeyIndexMapping.sGroupId] = [];
  		}
  		if (!this.mMultiUnitKey[oKeyIndexMapping.sGroupId]) {
  			this.mMultiUnitKey[oKeyIndexMapping.sGroupId] = [];
  		}
  
  		// need to adjust positions to point to the first multi-unit entry
  		// why? because in processGroupMemberQueryResponse(), advancing the mapping halts after(!) when the multi-unit case is detected, which happens with the second entry
  		--oKeyIndexMapping.iIndex;
  		--oKeyIndexMapping.iServiceKeyIndex;
  
  		this.mMultiUnitKey[oKeyIndexMapping.sGroupId][oKeyIndexMapping.iIndex] = this.oModel._getKey(oMultiUnitRepresentative.oEntry);
  
  		// the following setting in the key index serves two purposes: indicate the multi-unit situation and remember position of first related service key
  		this.mKeyIndex[oKeyIndexMapping.sGroupId][oKeyIndexMapping.iIndex++] = oKeyIndexMapping.iServiceKeyIndex > 0 ? -oKeyIndexMapping.iServiceKeyIndex : "ZERO";
  
  		// store service keys and consider new keys in the discarded count
  		var iNewServiceKeyIndexCount = 0;
  		for (var i = 0; i < aMultiUnitEntry.length; i++) {
  			if (!this.mServiceKey[oKeyIndexMapping.sGroupId][oKeyIndexMapping.iServiceKeyIndex]) {
  				++iNewServiceKeyIndexCount;
  			}
  			this.mServiceKey[oKeyIndexMapping.sGroupId][oKeyIndexMapping.iServiceKeyIndex++] = this.oModel._getKey(aMultiUnitEntry[i]);
  		}
  		return iNewServiceKeyIndexCount;
  	};
  
  	// combine loaded key index entries with a block of subsequent key index entries starting at the given key index mapping
  	AnalyticalBinding.prototype._mergeLoadedKeyIndexWithSubsequentIndexes = function(oKeyIndexMapping, aAggregationLevel, aSelectedUnitPropertyName, bIsFlatListRequest) {
  		/*
  		 * Note:
  		 * This is a complex algorithm with an external description.
  		 * You will need this description in order to understand this implementation.
  		 * The variable names are derived from the names used in the description, underscores in the names indicate indexed names.
  		 */
  		var aKI = this.mKeyIndex[oKeyIndexMapping.sGroupId], // is mKI in description
  			aSK = this.mServiceKey[oKeyIndexMapping.sGroupId], // is mSK in description
  			aMUK = this.mMultiUnitKey[oKeyIndexMapping.sGroupId], // is mMUK in description
  			iDiscardedEntriesCount = 0,
  			n_i = oKeyIndexMapping.iServiceKeyIndex,
  			n_e = oKeyIndexMapping.iIndex;
  
  		var	oMultiUnitRepresentative, oMultiUnitEntryKey;
  
  		if (aKI === undefined) {
  			return iDiscardedEntriesCount;
  		}
  
  		// step 1: determine if the adjacent service keys denote same dimension key and therefore must be merged
  		var bNeedMultiUnitKeyMerge = false;
  		var sPreviousServiceKey = aSK[n_i - 1],
  			sNextServiceKey = aSK[n_i];
  
  		if (sNextServiceKey === undefined) {
  			return iDiscardedEntriesCount;
  		}
  		if (sPreviousServiceKey === undefined) {
  			oLogger.fatal("assertion failed: missing expected entry before given key index");
  			return iDiscardedEntriesCount;
  		}
  		var oPreviousEntry = this.oModel.getObject("/" + sPreviousServiceKey);
  		var oNextEntry = this.oModel.getObject("/" + sNextServiceKey);
  
  		var sPreviousEntryDimensionKeyString = "",
  			sNextEntryDimensionKeyString = "";
  		for (var i = 0; i < aAggregationLevel.length; i++) {
  			sPreviousEntryDimensionKeyString += oPreviousEntry[aAggregationLevel[i]] + "|";
  			sNextEntryDimensionKeyString += oNextEntry[aAggregationLevel[i]] + "|";
  		}
  		bNeedMultiUnitKeyMerge = sPreviousEntryDimensionKeyString == sNextEntryDimensionKeyString;
  
  		// calculate nPrime_e for next steps
  		var nPrime_e = n_e;
  		if (nPrime_e >= this.mLength[oKeyIndexMapping.sGroupId]) {
  			oLogger.fatal("assertion failed: service key exists,but no corresponding key index found");
  // 			this._trace_debug_if(true, "assertion failed: service key exists,but no corresponding key index found");
  			return iDiscardedEntriesCount;
  		}
  		while (aKI[nPrime_e] === undefined || Math.abs(aKI[nPrime_e]) < n_i) {
  			++nPrime_e;
  		}
  
  // 		this._trace_enter("SvcDatCons", "_mergeLoadedKeyIndexWithSubsequentIndexes", "groupId=" + oKeyIndexMapping.sGroupId, { n_e: n_e, nPrime_e: nPrime_e, n_i: n_i }, ["n_e","nPrime_e","n_i"]); // DISABLED FOR PRODUCTION
  
  		// step 2: combine loaded key index entries with subsequent key index entries
  		if (bNeedMultiUnitKeyMerge) { // case 1
  			if (Math.abs(aKI[nPrime_e]) == n_i && aKI[nPrime_e] < 0) { // case a) nPrime_e is a multi-unit entry and starts at n_i
  // 				this._trace_debug_if(aKI[nPrime_e] >= 0 || aMUK[nPrime_e] === undefined, "unexpected: no multi-unit entry found");
  				if (nPrime_e > n_e) { // relevance check for merging the loaded key index section with subsequent indexes
  					if (aKI[n_e - 1] < 0) { // case I: (nPrime_e - 1) is a multi-unit entry
  // 						this._trace_message("SvcDatCons", "case 1.a.I"); // DISABLED FOR PRODUCTION
  						aMUK[nPrime_e] = undefined; // delete its multi-unit entry
  						// delete aKI entries n_e ... nPrime_e - 1 and at nPrime_e (this will remove the redundant second multi-unit entry at nPrime_e)
  						aKI.splice(n_e, nPrime_e - n_e + 1);
  						aMUK.splice(n_e, nPrime_e - n_e + 1);
  					} else { // case II: (nPrime_e - 1) is NOT a multi-unit entry
  // 						this._trace_message("SvcDatCons", "case 1.a.II"); // DISABLED FOR PRODUCTION
  						aKI[n_e - 1] = -aKI[n_e - 1]; // make n_e - 1 a multi-unit entry
  						aMUK[n_e - 1] = aMUK[nPrime_e]; // reuse aMUK[nPrime_e] for aMUK[n_e - 1]
  						aMUK[nPrime_e] = undefined; // clear aMUK[nPrime_e]
  						// delete aKI entries n_e ... nPrime_e - 1 and at nPrime_e (this will remove the redundant second multi-unit entry at nPrime_e)
  						aKI.splice(n_e, nPrime_e - n_e + 1);
  						aMUK.splice(n_e, nPrime_e - n_e + 1);
  						iDiscardedEntriesCount = 1;
  					}
  				}
  			} else if (Math.abs(aKI[nPrime_e]) > n_i) { // case b) nPrimePrime_e = nPrime_e - 1 is a multi-unit entry pointing to service keys before n_i
  				var nPrimePrime_e = nPrime_e - 1;
  // 				this._trace_debug_if(n_e == 314, "stop");
  // 				this._trace_debug_if(!(Math.abs(aKI[nPrimePrime_e]) < n_i), "unexpected: this key index must point to a key before n_i");
  
  				if (aKI[nPrimePrime_e] > 0) { // case I: (nPrimePrime_e) is not a multi-unit entry
  // 					this._trace_message("SvcDatCons", "case 1.b.I"); // DISABLED FOR PRODUCTION
  // 					this._trace_debug_if(aKI[nPrimePrime_e] < n_i - 1, "unexpected: this key index must point to the last read service key or before"); // this case would be 1 b) II ii
  					// create a multi-unit entry for nPrimePrime_e
  					oMultiUnitRepresentative = this._createMultiUnitRepresentativeEntry(oKeyIndexMapping.sGroupId, oPreviousEntry, aSelectedUnitPropertyName, undefined, bIsFlatListRequest);
  					oMultiUnitEntryKey = this.oModel._getKey(oMultiUnitRepresentative.oEntry);
  					// make nPrimePrime_e a multi-unit entry
  					aKI[nPrimePrime_e] = -aKI[nPrimePrime_e];
  					aMUK[nPrimePrime_e] = oMultiUnitEntryKey;
  					if (nPrimePrime_e > n_e) {
  						// delete aKI entries n_e ... nPrimePrime_e - 1
  						aKI.splice(n_e, nPrimePrime_e - n_e);
  						aMUK.splice(n_e, nPrimePrime_e - n_e);
  					}
  // 					this._trace_debug_if(Math.abs(aKI[nPrime_e]) - Math.abs(aKI[nPrimePrime_e]) <= 1, "unexpected: marked as multi-unit key, but only a single service key!?");
  					if (oMultiUnitRepresentative.bIsNewEntry) {
  // 						this._trace_debug_if(Math.abs(aKI[nPrime_e]) - Math.abs(aKI[nPrimePrime_e]) > 2, "unexpected: more than one subsequent service key for this multi-unit entry, but no representative so far!?");
  						// two service keys contributing to this multi-unit entry, and one new multi-unit representative => 1 more service key to discard
  						iDiscardedEntriesCount = 1;
  					} else {
  // 						this._trace_debug_if(oMultiUnitRepresentative.iIndex != nPrimePrime_e, "the existing multi-unit representative does not have index nPrimePrime_e");
  						// more than two service keys contributing to this multi-unit entry, which were already detected as "multi-unit" and therefore discarded.
  						// since the existing multi-unit representative has index nPrimePrime_e, the service key pointed to by this index was also already covered
  						// => 0 more service key to discard
  						iDiscardedEntriesCount = 0;
  					}
  				} else if (aKI[n_e - 1] < 0) {
  						// case II: (nPrimePrime_e) is a multi-unit entry
  						// case i: (n_e - 1) is a multi-unit entry
  						if (nPrime_e > n_e) { // relevance check for merging the loaded key index section with subsequent indexes
  // 							this._trace_message("SvcDatCons", "case 1.b.II.i"); // DISABLED FOR PRODUCTION
  							aMUK[nPrimePrime_e] = undefined; // delete its multi-unit entry
  							// delete aKI entries n_e ... nPrimePrime_e - 1 and at nPrimePrime_e (this will remove the redundant second multi-unit entry at nPrimePrime_e)
  							aKI.splice(n_e, nPrimePrime_e - n_e + 1);
  							aMUK.splice(n_e, nPrimePrime_e - n_e + 1);
  						}
  					} else { // case ii: (n_e - 1) is NOT a multi-unit entry
  // 						this._trace_message("SvcDatCons", "case 1.b.II.ii"); // DISABLED FOR PRODUCTION
  // 						this._trace_debug_if(aKI[n_e - 1] != Math.abs(aKI[nPrimePrime_e]), "unexpected: n_e - 1 does not point to same entry as nPrimePrime_e");
  // 						this._trace_debug_if(Math.abs(aKI[nPrimePrime_e]) != n_i - 1, "unexpected: nPrimePrime_e should point to n_i - 1");
  						aKI[n_e - 1] = -aKI[n_e - 1]; // make n_e - 1 a multi-unit entry
  						aMUK[n_e - 1] = aMUK[nPrimePrime_e]; // reuse aMUK[nPrimePrime_e] for aMUK[n_e - 1]
  						aMUK[nPrimePrime_e] = undefined; // clear aMUK[nPrimePrime_e]
  						// delete aKI entries n_e ... nPrime_e - 1 and at nPrimePrime_e (this will remove the redundant second multi-unit entry at nPrimePrime_e)
  						aKI.splice(n_e, nPrimePrime_e - n_e + 1);
  						aMUK.splice(n_e, nPrimePrime_e - n_e + 1);
  					}
  			} else if (aKI[nPrime_e] == n_i) { // case c) nPrime_e is NOT a multi-unit entry
  				if (nPrime_e > n_e) { // relevance check for merging the loaded key index section with subsequent indexes
  					if (aKI[n_e - 1] < 0) { // case I: (nPrime_e - 1) is a multi-unit entry
  // 						this._trace_message("SvcDatCons", "case 1.c.I"); // DISABLED FOR PRODUCTION
  						// delete aKI entries n_e ... nPrime_e - 1 and at nPrime_e (this will remove the redundant second multi-unit entry at nPrimePrime_e)
  						aKI.splice(n_e, nPrime_e - n_e + 1);
  						aMUK.splice(n_e, nPrime_e - n_e + 1);
  						iDiscardedEntriesCount = 1;
  					} else { // case II: (nPrime_e - 1) is NOT a multi-unit entry
  // 						this._trace_message("SvcDatCons", "case 1.c.II"); // DISABLED FOR PRODUCTION
  						// create a multi-unit entry for n_e - 1
  						oMultiUnitRepresentative = this._createMultiUnitRepresentativeEntry(oKeyIndexMapping.sGroupId, oPreviousEntry, aSelectedUnitPropertyName, undefined, bIsFlatListRequest);
  						oMultiUnitEntryKey = this.oModel._getKey(oMultiUnitRepresentative.oEntry);
  						if (!oMultiUnitRepresentative.bIsNewEntry) {
  							oLogger.fatal("assertion failed: multi-unit entry already existed before");
  // 							this._trace_debug_if(! oMultiUnitRepresentative.bIsNewEntry, "assertion failed: multi-unit entry already existed before");
  						}
  						// make n_e - 1 a multi-unit entry
  						aKI[n_e - 1] = -aKI[n_e - 1];
  						aMUK[n_e - 1] = oMultiUnitEntryKey;
  						// delete aKI entries n_e ... nPrime_e - 1 and at nPrime_e (this will remove the redundant second multi-unit entry at nPrimePrime_e)
  						aKI.splice(n_e, nPrime_e - n_e + 1);
  						aMUK.splice(n_e, nPrime_e - n_e + 1);
  						iDiscardedEntriesCount = 1;
  					}
  				}
  			} else {
  				oLogger.fatal("assertion failed: uncovered case detected");
  				// this._trace_debug_if(true, "assertion failed: uncovered case detected");
  				return iDiscardedEntriesCount;
  			}
  		} else if (aKI[nPrime_e] > n_i) {
  				// case 2
  
  //				case a)
  // 				this._trace_message("SvcDatCons", "case 2.a"); // DISABLED FOR PRODUCTION
  				oLogger.fatal("unstable query result for group ID " + oKeyIndexMapping.sGroupId + ": entries have been removed or added. Complete reload required");
  //				this._trace_debug_if(true, "unstable query result for group ID " + oKeyIndexMapping.sGroupId + ": entries have been removed or added. Complete reload required");
  			} else if (nPrime_e - n_e > 0) {
  				// case b)
  
  //				this._trace_message("SvcDatCons", "case 2.b"); // DISABLED FOR PRODUCTION
  //				delete aKI entries n_e ... nPrime_e - 1
  				aKI.splice(n_e, nPrime_e - n_e);
  //				this._trace_debug_if(aMUK === undefined, "unexpected: aMUK is undefined, so no multi-unit keys so far!?");
  				aMUK.splice(n_e, nPrime_e - n_e);
  			}
  
  // 		this._trace_leave("SvcDatCons", "_mergeLoadedKeyIndexWithSubsequentIndexes", "dicardedCount=" + iDiscardedEntriesCount); // DISABLED FOR PRODUCTION
  		return iDiscardedEntriesCount;
  	};
  
  
  	// create a local multi unit entry by copying the given reference entry, and modifying this new entry: clear all unit properties that are not part of the aggregation level, and all measures
  	// returns { oEntry, bIsNewEntry) the multi-unit representativ entry and a flag whether it already existed before this call
  	AnalyticalBinding.prototype._createMultiUnitRepresentativeEntry = function(sGroupId, oReferenceEntry, aSelectedUnitPropertyName, aDeviatingUnitPropertyName, bIsFlatListRequest) {
  		// set up properties for measures and units in this new entry
  		var oMultiUnitEntry = jQuery.extend(true, {}, oReferenceEntry);
  		var aReloadMeasurePropertyName = [];
  		for ( var sMeasureName in this.oMeasureDetailsSet) {
  			var oMeasureDetails = this.oMeasureDetailsSet[sMeasureName];
  			if (!bIsFlatListRequest && !this.mAnalyticalInfoByProperty[sMeasureName].total) {
  				if (oMeasureDetails.rawValuePropertyName != undefined) {
  					oMultiUnitEntry[oMeasureDetails.rawValuePropertyName] = undefined;
  				}
  				if (oMeasureDetails.formattedValuePropertyName != undefined) {
  					oMultiUnitEntry[oMeasureDetails.formattedValuePropertyName] = undefined;
  				}
  			} else {
  				if (oMeasureDetails.rawValuePropertyName != undefined) {
  					oMultiUnitEntry[oMeasureDetails.rawValuePropertyName] = null; // cannot be "*" because of type validation!
  				}
  				if (oMeasureDetails.formattedValuePropertyName != undefined) {
  					oMultiUnitEntry[oMeasureDetails.formattedValuePropertyName] = "*";
  				}
  			}
  			// determine if this measure that can be reloaded, because their unit properties do not have deviating values
  			if (aDeviatingUnitPropertyName) {
  				if (!oMeasureDetails.unitPropertyName || aDeviatingUnitPropertyName.indexOf(oMeasureDetails.unitPropertyName) == -1) {
  					aReloadMeasurePropertyName.push(oMeasureDetails.rawValuePropertyName);
  				}
  			}
  		}
  		for (var k = 0; k < aSelectedUnitPropertyName.length; k++) {
  			if (aDeviatingUnitPropertyName.indexOf(aSelectedUnitPropertyName[k]) != -1) {
  				oMultiUnitEntry[aSelectedUnitPropertyName[k]] = "*";
  			}
  		}
  
  		/*
  		 * assign a key to this new entry that allows to import it into the OData model that is guaranteed to be stable when used for multiple
  		 * bindings 1) Take all(!) grouping dimensions in alphabetical order of their names 2) Concatenate the values of these dimenensions in this
  		 * order separated by "," 3) append some indicator such as "-multiunit-not-dereferencable" to mark this special entry
  		 */
  		var sMultiUnitEntryKey = "";
  
  		for (var l = 0; l < this.aAllDimensionSortedByName.length; l++) {
  			var sDimVal = oMultiUnitEntry[this.aAllDimensionSortedByName[l]];
  			// if the value is an empty string, it should be treated as such in the generated key
  			var sSaveDimVal = sDimVal === "" ? '""' : sDimVal;
  			sSaveDimVal = sSaveDimVal === undefined ? "" : sSaveDimVal;
  			sMultiUnitEntryKey += (encodeURIComponent(sSaveDimVal) + ",");
  		}
  		sMultiUnitEntryKey += "-multiple-units-not-dereferencable";
  
  		// check if an entry already exists; if so, dont proceed, but return it
  		var iMultiUnitEntryIndex;
  		if (this.mMultiUnitKey[sGroupId] && (iMultiUnitEntryIndex = this.mMultiUnitKey[sGroupId].indexOf(sMultiUnitEntryKey)) != -1) {
  			return { oEntry: this.oModel.getObject("/" + sMultiUnitEntryKey), bIsNewEntry : false, iIndex: iMultiUnitEntryIndex, aReloadMeasurePropertyName: aReloadMeasurePropertyName }; // already created
  		}
  
  		// this modified copy must be imported to the OData model as a new entry with a modified key and OData metadata
  		oMultiUnitEntry.__metadata.uri = sMultiUnitEntryKey;
  		delete oMultiUnitEntry.__metadata["self"];
  		delete oMultiUnitEntry.__metadata["self_link_extensions"];
  		oMultiUnitEntry["^~volatile"] = true; // mark entry to distinguish it from others contained in the regular OData result
  
  		// 3rd argument: empty response, needed by the ODataModel, but we do not have a response, as we did not perform any requests.
  		this.oModel._importData(oMultiUnitEntry, {}, {});
  
  		// mark the context for this entry as volatile to facilitate special treatment by consumers
  		var sMultiUnitEntryModelKey = this.oModel._getKey(oMultiUnitEntry);
  		this.oModel.getContext('/' + sMultiUnitEntryModelKey)["_volatile"] = true;
  		return { oEntry: oMultiUnitEntry, bIsNewEntry : true, aReloadMeasurePropertyName: aReloadMeasurePropertyName };
  	};
  
  
  
  	//********************************
  	//*** Miscellaneous
  	//********************************/
  
  	AnalyticalBinding.prototype._clearAllPendingRequests = function() {
  		this.oPendingRequests = {};
  		this.oGroupedRequests = {};
  	};
  
  	/**
  	 * Resets the current list data and length
  	 *
  	 * @private
  	 */
  	AnalyticalBinding.prototype.resetData = function(oContext) {
  		var sGroupId = oContext ? oContext.getPath() : undefined;
  		this._resetData(sGroupId);
  	};
  
  	AnalyticalBinding.prototype._resetData = function(sGroupId) {
  		if (sGroupId) {
  			// reset only specific content
  			delete this.mServiceKey[sGroupId];
  			delete this.mServiceLength[sGroupId];
  			delete this.mServiceFinalLength[sGroupId];
  
  			delete this.mKeyIndex[sGroupId];
  			delete this.mLength[sGroupId];
  
  			delete this.mMultiUnitKey[sGroupId];
  			delete this.mEntityKey[sGroupId];
  		} else {
  			this.mServiceKey = {};
  			this.mServiceLength = {};
  			this.mServiceFinalLength = {};
  			this.mFinalLength = this.mServiceFinalLength;
  
  			this.mKeyIndex = {};
  			this.mLength = {};
  
  			this.mMultiUnitKey = {};
  
  			this.mEntityKey = {};
  		}
  	};
  
  	/**
  	 * Refreshes the binding, check whether the model data has been changed and fire change event if this is the case. For service side models this should refetch
  	 * the data from the service. To update a control, even if no data has been changed, e.g. to reset a control after failed validation, please use the parameter
  	 * bForceUpdate.
  	 *
  	 * @param {boolean}
  	 *            [bForceUpdate] Update the bound control even if no data has been changed
  	 * @public
  	 */
  	AnalyticalBinding.prototype.refresh = function(bForceUpdate) {
  		// apply is used here to be compatible to ODataModel v1, where the signature is like the private _refresh()
  		AnalyticalBinding.prototype._refresh.apply(this, arguments);
  	};
  
  	/**
  	 * @private
  	 */
  	AnalyticalBinding.prototype._refresh = function(bForceUpdate, mChangedEntities, mEntityTypes) {
  		var bChangeDetected = false;
  		if (!bForceUpdate) {
  			if (mEntityTypes) {
  				var sResolvedPath = this.oModel.resolve(this.sPath, this.oContext);
  				var oEntityType = this.oModel.oMetadata._getEntityTypeByPath(sResolvedPath);
  				if (oEntityType && (oEntityType.entityType in mEntityTypes)) {
  					bChangeDetected = true;
  				}
  			}
  			if (mChangedEntities && !bChangeDetected) {
  				jQuery.each(this.mServiceKey, function(i, aNodeKeys) {
  					jQuery.each(aNodeKeys, function(i, sKey) {
  						if (sKey in mChangedEntities) {
  							bChangeDetected = true;
  							return false;
  						}
  					});
  					if (bChangeDetected) {
  						return false;
  					}
  				});
  			}
  			if (!mChangedEntities && !mEntityTypes) { // default
  				bChangeDetected = true;
  			}
  		}
  		if (bForceUpdate || bChangeDetected) {
  			this._abortAllPendingRequests();
  			this.resetData();
  			this.bNeedsUpdate = false;
  			this._fireRefresh({reason: ChangeReason.Refresh});
  		}
  	};
  
  	/**
  	 * Check whether this Binding would provide new values and in case it changed, inform interested parties about this.
  	 *
  	 * @param {boolean}
  	 *            bForceUpdate
  	 * @param {object} mChangedEntities
  	 * @private
  	 */
  	AnalyticalBinding.prototype.checkUpdate = function(bForceUpdate, mChangedEntities) {
  		var bChangeDetected = false;
  		if (!bForceUpdate) {
  			if (this.bNeedsUpdate || !mChangedEntities) {
  				bChangeDetected = true;
  			} else {
  				jQuery.each(this.mServiceKey, function(i, aNodeKeys) {
  					jQuery.each(aNodeKeys, function(i, sKey) {
  						if (sKey in mChangedEntities) {
  							bChangeDetected = true;
  							return false;
  						}
  					});
  					if (bChangeDetected) {
  						return false;
  					}
  				});
  			}
  		}
  		if (bForceUpdate || bChangeDetected) {
  			this.bNeedsUpdate = false;
  			this._fireChange({reason: ChangeReason.Change});
  		}
  	};
  
  	/**
  	 * Get a download URL with the specified format considering the
  	 * sort/filter/custom parameters.
  	 *
  	 * The download URL also takes into account the selected dimensions and measures,
  	 * depending on the given column definitions of the AnalyticalTable.
  	 * This is based on the visible/inResult flags of the columns, as well as integrity dependencies,
  	 * e.g. for mandatory Unit properties.
  	 *
  	 * @param {string} sFormat Value for the $format Parameter
  	 * @return {string} URL which can be used for downloading
  	 * @since 1.24
  	 * @public
  	 */
  	AnalyticalBinding.prototype.getDownloadUrl = function(sFormat) {
  		var aSelectProperties, sProperty, z;
  
  		// create a new request
  		var oAnalyticalQueryRequest = new odata4analytics.QueryResultRequest(this.oAnalyticalQueryResult);
  		oAnalyticalQueryRequest.setResourcePath(this._getResourcePath());
  
  		// add current list of dimensions
  		var aSelectedDimension = [];
  		var aSelectedMeasure = [];
  		for (var oDimensionName in this.oDimensionDetailsSet) {
  			aSelectedDimension.push(oDimensionName);
  		}
  		oAnalyticalQueryRequest.setAggregationLevel(aSelectedDimension);
  		for (var oDimensionName2 in this.oDimensionDetailsSet) {
  			var oDimensionDetails = this.oDimensionDetailsSet[oDimensionName2];
  			var bIncludeText = (oDimensionDetails.textPropertyName != undefined);
  			oAnalyticalQueryRequest.includeDimensionKeyTextAttributes(oDimensionDetails.name, // bIncludeKey: No, always needed!
  					true, bIncludeText, oDimensionDetails.aAttributeName);
  		}
  
  		// add current list of measures
  		for (var sMeasureName in this.oMeasureDetailsSet) {
  			aSelectedMeasure.push(sMeasureName);
  		}
  		oAnalyticalQueryRequest.setMeasures(aSelectedMeasure);
  		for ( var sMeasureName2 in this.oMeasureDetailsSet) {
  			var oMeasureDetails = this.oMeasureDetailsSet[sMeasureName2];
  			var bIncludeRawValue = (oMeasureDetails.rawValuePropertyName != undefined);
  			var bIncludeFormattedValue = (oMeasureDetails.formattedValuePropertyName != undefined);
  			var bIncludeUnitProperty = (oMeasureDetails.unitPropertyName != undefined);
  			oAnalyticalQueryRequest.includeMeasureRawFormattedValueUnit(oMeasureDetails.name, bIncludeRawValue,
  					bIncludeFormattedValue, bIncludeUnitProperty);
  		}
  
  		// add the sorters
  		var oSortExpression = oAnalyticalQueryRequest.getSortExpression();
  		oSortExpression.clear();
  		for (var i = 0; i < this.aSorter.length; i++) {
  			if (this.aSorter[i]) {
  				oSortExpression.addSorter(this.aSorter[i].sPath, this.aSorter[i].bDescending ? odata4analytics.SortOrder.Descending : odata4analytics.SortOrder.Ascending);
  			}
  		}
  
  		// add the filters
  		var oFilterExpression = oAnalyticalQueryRequest.getFilterExpression();
  		oFilterExpression.clear();
  		if (this.aApplicationFilter) {
  			oFilterExpression.addUI5FilterConditions(this.aApplicationFilter);
  		}
  		if (this.aControlFilter) {
  			oFilterExpression.addUI5FilterConditions(this.aControlFilter);
  		}
  
  		// determine the entityset path incl. the required params (sort, filter, ...)
  		var sPath = oAnalyticalQueryRequest.getURIToQueryResultEntitySet();
  		// always consider additional selects for download URL
  		var aParam = this._getQueryODataRequestOptions(oAnalyticalQueryRequest, true);
  
  		if (!aParam) {
  			// parameters could not be determined correctly
  			return undefined;
  		}
  
  		// add the new $select param which is sorted like the Table
  		var aExportCols = [];
  		for (var k = 0, m = this.aAnalyticalInfo.length; k < m; k++) {
  			var oCol = this.aAnalyticalInfo[k];
  			if ((oCol.visible || oCol.inResult)
  					&& oCol.name !== ""
  					&& oCol.name !== aExportCols[aExportCols.length - 1]) {
  				aExportCols.push(oCol.name);
  
  				// add belonging currency column implicitly if present
  				if (this.oMeasureDetailsSet[oCol.name] != undefined
  					&& this.oMeasureDetailsSet[oCol.name].unitPropertyName != undefined) {
  					aExportCols.push(this.oMeasureDetailsSet[oCol.name].unitPropertyName);
  				}
  			}
  		}
  
  		// search and replace the $select
  		for (var j = 0, l = aParam.length; j < l; j++) {
  			if (/^\$select/i.test(aParam[j])) {
  				if (this.mParameters.select) {
  					// merge export columns with the computed $select only if select binding
  					// parameter is given
  					aSelectProperties = aParam[j].slice(8).split(",");
  					for (z = 0; z < aSelectProperties.length; z++) {
  						sProperty = aSelectProperties[z];
  						if (aExportCols.indexOf(sProperty) === -1) {
  							aExportCols.push(sProperty);
  						}
  					}
  				}
  				aParam[j] = "$select=" + aExportCols.join(",");
  				break;
  			}
  		}
  
  		// insert the format as first parameter
  		if (sFormat) {
  			aParam.splice(0, 0, "$format=" + encodeURIComponent(sFormat));
  		}
  
  		// add the custom url parameters
  		if (this.sCustomParams) {
  			aParam.push(this.sCustomParams);
  		}
  
  		// create the request URL
  		if (sPath) {
  			return this.oModel._createRequestUrl(sPath, null, aParam).replace(/ /g, "%20");
  		}
  
  	};
  
  	//********************************
  	//*** Tracing execution
  	//********************************/
  
  	/** DISABLED FOR PRODUCTION
  // 	 *    to enable, search using regex for "^// (.*\._trace_.*)", replace by "$1"
  // 	 *    to disable, search using regex for "^(.*\._trace_.*)", replace by "// $1"
  	 *
  // 	AnalyticalBinding.prototype._trace_enter = function(groupid, scope, input_msg, _arguments, arg_components) {
  		if (!this._traceMsgCtr) {
  			this._traceMsgCtr = { level: 0, msg: [] };
  		}
  		this._traceMsgCtr.msg.push( { group: groupid, level: ++this._traceMsgCtr.level, scope: scope, msg: input_msg, details: _arguments, arg_components: arg_components, enter: true } );
  	};
  
  // 	AnalyticalBinding.prototype._trace_leave = function (groupid, scope, output_msg, results, arg_components) {
  		if (!this._traceMsgCtr) {
  			throw "leave without enter";
  		}
  		this._traceMsgCtr.msg.push( { group: groupid, level: this._traceMsgCtr.level--, scope: scope, msg: output_msg, details: results, arg_components: arg_components, leave: true } );
  	};
  
  // 	AnalyticalBinding.prototype._trace_message = function (groupid, input_msg, _arguments, arg_components) {
  		if (!this._traceMsgCtr) {
  			throw "message without enter";
  		}
  		this._traceMsgCtr.msg.push( { group: groupid, level: this._traceMsgCtr.level, msg: input_msg, details: _arguments, arg_components: arg_components } );
  	};
  
  // 	AnalyticalBinding.prototype._trace_if_message = function (condition, groupid, message, details) {
  		if (condition) {
  // 			this._trace_message(groupid, message, details);
  		}
  	};
  
  // 	AnalyticalBinding.prototype._trace_debug_if = function (condition, message, details) {
  // 		if (this._trace_debug_switch === undefined) {
  // 			this._trace_debug_switch = true;
  		}
  // 		if (this._trace_debug_switch && condition) {
  			debugger;
  		}
  	};
  
  // 	AnalyticalBinding.prototype._trace_debug = function () {
  		debugger;
  	};
  
  // 	AnalyticalBinding.prototype._trace_debug_switch = function (onoroff) {
  // 		this._trace_debug_switch = onoroff;
  	};
  
  // 	AnalyticalBinding.prototype._trace_dump = function (aGroupId) {
  		var fRenderMessage = function (line) {
  			var s = "[" + line.group + "          ".slice(0,10 - line.group.length) + "]";
  			for (var i = 0; i < line.level; i++) {
  				s += "  ";
  			}
  			if (line.enter) {
  				s += "->" + line.scope + (line.msg || line.arg_components ? ":\t" : "");
  			}
  			else if (line.leave) {
  				s += "<-" + line.scope + (line.msg || line.arg_components ? ":\t" : "");
  			} else {
  				s += "  ";
  			}
  			if (line.msg) {
  				s += line.msg + ",";
  			}
  			if (line.details && line.arg_components) {
  				for (var j = 0; j < line.arg_components.length; j++) {
  					s += line.arg_components[j] + "=" + eval("line.details." + line.arg_components[j]) + (j < line.arg_components.length - 1 ? "," : "");
  				}
  			}
  			s += "\n";
  			return s;
  		};
  		var fRender = function (aMsg) {
  			var s = "";
  			for (var i = 0; i < aMsg.length; i++) {
  				if (!aGroupId || jQuery.inArray(aMsg[i].group, aGroupId) != -1) {
  					s += fRenderMessage(aMsg[i]);
  				}
  			}
  			return s;
  		};
  		if (!this._traceMsgCtr) {
  			return "";
  		}
  		return "\n" + fRender(this._traceMsgCtr.msg);
  	};
  
  // 	AnalyticalBinding.prototype._trace_reset = function () {
  		delete this._traceMsgCtr;
  	};
  
  	**/
  
  	//**********************************
  	//*** Grouping together with Sorting
  	//**********************************
  	/**
  	 * Adds the given sorters and 'this.aSorter' to the given sort expression object.
  	 * Depending on the result of {@link #_canApplySortersToGroups}, 'this.aSorter' are added
  	 * before the given sorters or after them.
  	 *
  	 * @param {sap.ui.model.analytics.odata4analytics.SortExpression} oSortExpression
  	 *    The sort expression
  	 * @param {sap.ui.model.Sorter[]} aGroupingSorters
  	 *    An array of sorter objects resulting from grouping
  	 * @private
  	 */
  	AnalyticalBinding.prototype._addSorters = function (oSortExpression, aGroupingSorters) {
  		var aSorters = this._canApplySortersToGroups()
  				? [].concat(this.aSorter).concat(aGroupingSorters)
  				: [].concat(aGroupingSorters).concat(this.aSorter);
  
  		aSorters.forEach(function (oSorter) {
  			oSortExpression.addSorter(oSorter.sPath, oSorter.bDescending
  				? odata4analytics.SortOrder.Descending : odata4analytics.SortOrder.Ascending);
  		});
  	};
  
  	/**
  	 * Returns whether sorters in 'this.aSorter' can be applied to the groups. This feature is only
  	 * enabled if binding's auto expand mode is set to 'Sequential'.
  	 * Logs a warning if applying sorters to groups is not possible because of auto expand mode.
  	 * Do not log the warning twice if auto expand mode does not change.
  	 *
  	 * @returns {boolean} Whether 'this.aSorter' can be applied to the groups
  	 * @private
  	 */
  	AnalyticalBinding.prototype._canApplySortersToGroups = function () {
  		var sCurrentAutoExpandMode = this._autoExpandMode;
  
  		if (this.bApplySortersToGroups) {
  			if (this.aSorter.length > 0) {
  				// check whether to log a warning and update sLastAutoExpandMode
  				if (sCurrentAutoExpandMode !== this.sLastAutoExpandMode
  						&& sCurrentAutoExpandMode !== TreeAutoExpandMode.Sequential) {
  					oLogger.warning("Applying sorters to groups is only possible with auto"
  						+ " expand mode 'Sequential'; current mode is: " + sCurrentAutoExpandMode,
  						this.sPath);
  				}
  				this.sLastAutoExpandMode = sCurrentAutoExpandMode;
  			}
  			return sCurrentAutoExpandMode === TreeAutoExpandMode.Sequential;
  		}
  		return false;
  	};
  
  	/**
  	 * Resets the flag that sorters can be applied to groups and logs a warning if not yet done.
  	 *
  	 * @param {string} sDetails Details for the warning
  	 * @private
  	 */
  	AnalyticalBinding.prototype._warnNoSortingOfGroups = function (sDetails) {
  		var sMessage;
  
  		if (this.bApplySortersToGroups) {
  			sMessage = "Detected a multi-unit case, so sorting is only possible on leaves";
  			if (sDetails) {
  				sMessage += "; " + sDetails;
  			}
  			oLogger.warning(sMessage, this.sPath);
  		}
  		this.bApplySortersToGroups = false;
  	};
  
  	AnalyticalBinding.Logger = oLogger;
  
  	return AnalyticalBinding;
  });