@openui5/sap.ui.core
Version:
OpenUI5 Core Library sap.ui.core
1,165 lines (1,067 loc) • 217 kB
JavaScript
/*!
* OpenUI5
* (c) Copyright 2026 SAP SE or an SAP affiliate company.
* Licensed under the Apache License, Version 2.0 - see LICENSE.txt.
*/
// Disable some ESLint rules. camelcase (some "_" in names to indicate indexed variables (like in math)),
// valid-jsdoc (not completed yet)
/*eslint camelcase:0, valid-jsdoc:0, max-len:0 */
// Provides class sap.ui.model.odata.ODataListBinding
sap.ui.define([
"./BatchResponseCollector",
"./odata4analytics",
"sap/base/Log",
"sap/base/util/deepExtend",
"sap/base/util/each",
"sap/base/util/extend",
"sap/base/util/isEmptyObject",
"sap/base/util/uid",
"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/TreeAutoExpandMode",
"sap/ui/model/TreeBinding",
"sap/ui/model/odata/CountMode",
"sap/ui/model/odata/ODataUtils"
], function(BatchResponseCollector, odata4analytics, Log, deepExtend, each, extend, isEmptyObject, uid,
ChangeReason, Filter, FilterOperator, FilterProcessor, FilterType, Sorter, TreeAutoExpandMode,
TreeBinding, CountMode, ODataUtils) {
"use strict";
var sClassName = "sap.ui.model.analytics.AnalyticalBinding",
iInstanceCount = 0,
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, unless the dimension or measure has been added automatically by the
* binding, because a property associated to the dimension or measure has been added as a
* "visible" or "inResult" column
* </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 oColumn, aComputedSelect, sComputedSelect, oDimension, i, j, oMeasure, n, sPropertyName,
oAnalyticalQueryRequest
= new odata4analytics.QueryResultRequest(oBinding.oAnalyticalQueryResult),
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) {
oColumn = oBinding.mAnalyticalInfoByProperty[sPropertyName];
if (!oColumn || (!oColumn.visible && !oColumn.inResult)) {
continue; // ignore automatically added columns
}
oLogger.warning("Ignored the 'select' binding parameter, because"
+ " it does not contain the property '" + sPropertyName + "'",
oBinding.sPath);
bError = true;
} else {
aSelect.splice(j, 1);
}
}
}
const aAdditionalSelects = [];
// check additionally selected properties, no new dimensions and new measures or
// associated properties for new dimensions or measures are allowed
for (i = 0; i < aSelect.length; i += 1) {
sPropertyName = aSelect[i];
oDimension = oBinding.oAnalyticalQueryResult.findDimensionByPropertyName(sPropertyName);
if (oDimension) {
const oDimensionDetails = oBinding.oDimensionDetailsSet[oDimension.getName()];
if (oDimensionDetails === undefined) {
logUnsupportedPropertyInSelect(oBinding.sPath, sPropertyName, oDimension);
bError = true;
// eslint-disable-next-line no-use-before-define
} else if (AnalyticalBinding._updateDimensionDetailsTextProperty(oDimension, sPropertyName,
oDimensionDetails)) {
continue;
}
}
oMeasure = oBinding.oAnalyticalQueryResult.findMeasureByPropertyName(sPropertyName);
if (oMeasure && oBinding.oMeasureDetailsSet[oMeasure.getName()] === undefined) {
logUnsupportedPropertyInSelect(oBinding.sPath, sPropertyName, oMeasure);
bError = true;
}
aAdditionalSelects.push(sPropertyName);
}
return bError ? [] : aAdditionalSelects;
}
/**
* 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 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 <code>None</code>, 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 is logged if it is <code>None</code>, because the binding still adds the
* $inlinecount to OData requests. If a binding count mode is set to <code>Request</code> or
* <code>Both</code>, a warning is logged to remind the application that the OData requests
* generated by the AnalyticalBinding include a $inlinecount.
*
* @param {sap.ui.model.Model} oModel
* The OData model
* @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 {sap.ui.model.Sorter[]|sap.ui.model.Sorter} [aSorters=[]]
* The sorters used initially; call {@link #sort} to replace them
* @param {sap.ui.model.Filter[]|sap.ui.model.Filter} [aFilters=[]]
* The filters to be used initially with type {@link sap.ui.model.FilterType.Application}; call {@link #filter} to
* replace them
* @param {object} [mParameters=null]
* A map containing additional binding parameters; for the <code>AnalyticalBinding</code> this
* parameter is mandatory
* @param {sap.ui.model.TreeAutoExpandMode} [mParameters.autoExpandMode=sap.ui.model.TreeAutoExpandMode.Bundled]
* The auto expand mode; applying sorters to groups is only possible in auto expand mode
* {@link sap.ui.model.TreeAutoExpandMode.Sequential}
* @param [mParameters.entitySet]
* The entity set addressed by the last segment of the given binding path
* @param [mParameters.useBatchRequests=false]
* Whether multiple OData requests are wrapped into a single $batch request wherever possible
* @param [mParameters.provideGrandTotals=true]
* Whether grand total values are provided for all bound measure properties
* @param [mParameters.provideTotalResultSize=true]
* Whether the total number of matching entries in the bound OData entity set is provided
* @param [mParameters.reloadSingleUnitMeasures=true]
* Whether the binding checks 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}). 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 {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), or if the {@link sap.ui.model.Filter.NONE} filter instance is
* contained in <code>aFilters</code> together with other filters, or if the given model is not supported.
*
* @alias sap.ui.model.analytics.AnalyticalBinding
* @extends sap.ui.model.TreeBinding
* @deprecated As of version 1.138.0, will be replaced by OData V4 data aggregation, see
* {@link topic:7d914317c0b64c23824bf932cc8a4ae1 Extension for Data Aggregation}
* @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.iModelVersion = AnalyticalBinding._getModelVersion(this.oModel);
if (this.iModelVersion === null) {
throw new Error("The AnalyticalBinding does not support the given model");
}
this.aAdditionalSelects = [];
// attribute members for addressing the requested entity set
this.sEntitySetName = 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 : [];
if (!Array.isArray(this.aSorter)) {
this.aSorter = [this.aSorter];
}
this.aMaxAggregationLevel = [];
this.aAggregationLevel = [];
this.oPendingRequests = {};
this.oPendingRequestHandle = [];
this.oGroupedRequests = {};
this.bUseBatchRequests = mParameters.useBatchRequests === true;
this.bProvideTotalSize = mParameters.provideTotalResultSize !== false;
this.bProvideGrandTotals = mParameters.provideGrandTotals !== false;
this.bReloadSingleUnitMeasures = mParameters.reloadSingleUnitMeasures !== false;
this.bUseAcceleratedAutoExpand = mParameters.useAcceleratedAutoExpand !== false;
this.bNoPaging = mParameters.noPaging === true;
iInstanceCount += 1;
this._iId = iInstanceCount;
// 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.countMode == CountMode.None) {
oLogger.fatal("requested count mode is ignored; OData requests will include"
+ " $inlinecount options");
} else if (mParameters.countMode == CountMode.Request || mParameters.countMode == CountMode.Both) {
oLogger.warning("default count mode is ignored; OData requests will include"
+ " $inlinecount options");
} else if (this.oModel.sDefaultCountMode == CountMode.Request) {
oLogger.warning("default count mode is ignored; OData requests will include"
+ " $inlinecount options");
}
// 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.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;
this.iTotalSize = -1; // invalidate last row counter
this._abortAllPendingRequests();
// resolving the path makes sure that we can safely analyze the metadata,
// as we have a resourcepath for the QueryResult
sResolvedPath = this.getResolvedPath();
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
*/
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.AnalyticalBinding#initialize or
* sap.ui.model.analytics.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 {object|int} [mParameters=0]
* Parameter map specifying how the topmost aggregation level shall be fetched. If this
* parameter map is set, the optional function parameters are ignored. Optionally, instead
* of a parameter map an integer value can be set to define the parameter
* <code>startIndex</code> as described in this parameter list. In this case, the function
* parameters <code>iLength</code>, <code>iNumberOfExpandedLevels</code> and
* <code>iThreshold</code> become mandatory.
* @param {int} mParameters.length
* Number of entries to return at and after the given start index; defaults to the model's
* size limit, see {@link sap.ui.model.Model#setSizeLimit}
* @param {int} mParameters.numberOfExpandedLevels
* Number of child levels that shall be fetched automatically
* @param {int} mParameters.startIndex
* Index of first entry to return from parent group ID <code>"/"</code> (zero-based)
* @param {int} mParameters.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
* @param {int} [iLength]
* See documentation of the <code>length</code> parameter in the parameter list of
* <code>mParameters</code>
* @param {int} [iNumberOfExpandedLevels=0]
* See documentation of the <code>numberOfExpandedLevels</code> parameter in the parameter
* list of <code>mParameters</code>
* @param {int} [iThreshold=0]
* See documentation of the <code>threshold</code> parameter in the parameter list of
* <code>mParameters</code>
* @return {sap.ui.model.Context[]}
* 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, iLength,
iNumberOfExpandedLevels, iThreshold) {
if (typeof mParameters !== "object") {
mParameters = {
length : iLength,
numberOfExpandedLevels : iNumberOfExpandedLevels,
startIndex : mParameters,
threshold : iThreshold
};
}
if (this.isInitial()) {
return [];
}
var iAutoExpandGroupsToLevel = (mParameters && mParameters.numberOfExpandedLevels ? mParameters.numberOfExpandedLevels + 1 : 1);
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)) {
return aRootContext;
}
aRootContext = this._getContextsForParentContext(null);
if (aRootContext.length == 1) {
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
});
}
if (aRootContext.length > 1) {
oLogger.fatal("assertion failed: grand total represented by a single entry");
}
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 {sap.ui.model.Context} oContext
* Parent context identifying the requested group of child contexts
* @param {object|int} [mParameters=0]
* Parameters, specifying the aggregation level for which contexts shall be fetched
* or the index of the first child entry to return from the parent contexts
* @param {int} mParameters.level
* Level number for oContext, because it might occur at multiple levels; context with group ID <code>"/"</code> has level 0
* @param {int} [mParameters.numberOfExpandedLevels=0]
* Number of child levels that shall be fetched automatically
* @param {int} [mParameters.startIndex=0]
* Index of first child entry to return from the parent context (zero-based)
* @param {int} [mParameters.length=<model size limit>]
* Number of entries to return; counting begins at the given start index
* @param {int} [mParameters.threshold=0]
* Number of additional entries that shall be locally available in the binding for subsequent
* accesses to child entries of the given parent context
* @param {int} [iLength=<model size limit>]
* Same meaning as <code>mParameters.length</code>, legacy signature variant only
* @param {int} [iThreshold=0]
* Same meaning as <code>mParameters.threshold</code>, legacy signature variant only
* @param {int} [iLevel]
* Same meaning as <code>mParameters.level</code>, legacy signature variant only
* @param {int} [iNumberOfExpandedLevels=0]
* Same meaning as <code>mParameters.numberOfExpandedLevels</code>, legacy signature variant only
* @returns {sap.ui.model.Context[]}
* 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 [];
}
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 {
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);
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 leaves or <code>undefined</code> if this is unknown.
*
* @return {number|undefined}
* The total number of leaves, or <code>undefined</code> if the number is not yet known or if
* the <code>provideTotalResultSize</code> binding parameter is set to <code>false</code>
*
* @public
* @see sap.ui.model.odata.v4.ODataListBinding#getCount
* @since 1.92.0
*/
AnalyticalBinding.prototype.getCount = function () {
return this.iTotalSize >= 0 ? this.iTotalSize : undefined;
};
/**
* Gets the total number of leaves or <code>-1</code> if this is unknown.
*
* @return {number}
* The total number of leaves, or <code>-1</code> if the number is not yet known or if the
* binding parameter <code>provideTotalResultSize</code> is set to <code>false</code>
*
* @public
* @deprecated Since 1.92 use {@link #getCount} instead
*/
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 {object} [mParameters]
* The only supported parameter is <code>level</code> as the level number of oContext (because
* the context might occur at multiple levels)
* @param {int} [mParameters.level=1]
* The aggregation level number
* @return {boolean}
* <code>true</code> if any of the contexts in the specified group has further children
* @public
*/
AnalyticalBinding.prototype.hasChildren = function(oContext, mParameters) {
mParameters = mParameters || {level : 1};
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 {object}
* 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 {object}
* 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} [aFilters=[]]
* The filters to use; in case of type {@link sap.ui.model.FilterType.Application} this replaces the filters
* given in {@link sap.ui.model.odata.v2.ODataModel#bindList}; a falsy value is treated as an empty array and thus
* removes all filters of the specified type
* @param {sap.ui.model.FilterType}
* [sFilterType=sap.ui.model.FilterType.Control] Type of the filter which should be adjusted.
* @return {this}
* 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[]|sap.ui.model.Sorter} [aSorter=[]]
* The sorters to use; they replace the sorters given in {@link sap.ui.model.odata.v2.ODataModel#bindList}; a
* falsy value is treated as an empty array and thus removes all sorters
* @return {this}
* 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><label>:<key-value>[-<text-value>]</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),
sFormattedPropertyValue, sFormattedTextPropertyValue, sGroupName, sLabelText,
oTextProperty, sTextProperty, sTextPropertyValue, fTextValueFormatter;
if (oDimension && this.oDimensionDetailsSet[sGroupProperty].textPropertyName) {
oTextProperty = oDimension.getTextProperty();
}
if (oTextProperty) {
sTextProperty = oTextProperty.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);
sFormattedPropertyValue = fValueFormatter
? fValueFormatter(sPropertyValue, sTextPropertyValue) : sPropertyValue;
sFormattedTextPropertyValue = fTextValueFormatter
? fTextValueFormatter(sTextPropertyValue, sPropertyValue) : sTextPropertyValue;
} else {
sFormattedPropertyValue = fValueFormatter
? fValueFormatter(sPropertyValue) : sPropertyValue;
}
sLabelText = oDimension.getLabelText && oDimension.getLabelText();
sGroupName = (sLabelText ? sLabelText + ': ' : '') + sFormattedPropertyValue;
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 <code>name</code> of the bound property and in
* addition:
* <ol>
* <li>A column bound to a dimension property has further boolean properties:
* <ul>
* <li>grouped: dimension is used for building groups</li>
* <li>inResult: if the column is not visible, but declared to be part of the result,
* values for the related property are also fetched from the OData service</li>
* <li>visible: if the column is visible, values