@openui5/sap.ui.core
Version:
OpenUI5 Core Library sap.ui.core
1,174 lines (1,073 loc) • 222 kB
JavaScript
/*!
* OpenUI5
* (c) Copyright 2009-2021 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), 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/isEmptyObject',
'sap/base/util/uid',
'sap/ui/thirdparty/jquery',
'sap/base/Log'
], function(
TreeBinding,
ChangeReason,
Filter,
FilterOperator,
FilterProcessor,
FilterType,
Sorter,
CountMode,
TreeAutoExpandMode,
odata4analytics,
BatchResponseCollector,
AnalyticalVersionInfo,
isEmptyObject,
uid,
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 <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 {array} [aSorter=null]
* An array of predefined sorters
* @param {array} [aFilters=null]
* An array of predefined filters
* @param {object} [mParameters=null]
* A map containing additional binding parameters
* @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}) 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 {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;
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.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 {object}
* 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 {object}
* 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.
*
* @return {number}
* The total number of addressed entities in the OData entity set, or -1 if the number is
* not yet known or if the binding parameter <code>provideTotalResultSize</code> is set to
* <code>false</code>
*
* @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 {object}
* 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 {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}
* 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 {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|array}
* aSorter a sorter object or an array of sorter objects which define the sort order.
* @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 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 iDiff,
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 (oP