Alexander Shabarshov 2a7a24c9e7 Initial contribution
2025-11-03 14:43:26 +00:00

379 lines
16 KiB
C#
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* dbMango
*
* Copyright 2025 Deutsche Bank AG
* SPDX-License-Identifier: Apache-2.0
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
using System.Reflection;
using log4net;
namespace Rms.Risk.Mango.Pivot.Core.Models;
/// <summary>
/// Implements drilldown functionality.
/// </summary>
// ReSharper disable once InconsistentNaming
public class DrilldownSupport(List<GroupedCollection> _collections)
{
private static readonly ILog _log = LogManager.GetLogger(MethodBase.GetCurrentMethod()!.DeclaringType!);
public const string DefaultDrilldownField = "<Default>";
public Func<KeyValuePair<string, object>[], Task> ShowDocument { get; set; } = _ => Task.CompletedTask;
public Func<string, Task> MessageBoxShow { get; set; } = _ => Task.CompletedTask;
public Func<string, string, Task<bool>> MessageBoxShowYesNo { get; set; } = (_,_) => Task.FromResult(false);
public Func<Exception, Task> ShowException { get; set; } = _ => Task.CompletedTask;
public Func<string, string, PivotDefinition?> GetPivotDefinition { get; set; } = (_,_) => null;
public Func<string, string, Task<Tuple<string,PivotDefinition>?>> GetCustomDrilldown { get; set; } = (_,_) => Task.FromResult<Tuple<string,PivotDefinition>?>(null);
private GroupedCollection GetCollection(string name) => _collections.FirstOrDefault( x => x.CollectionNameWithPrefix == name ) ?? throw new ApplicationException($"Collection=\"{name}\" is not found");
// ReSharper disable once UnusedAutoPropertyAccessor.Global
public static bool ShowAllKeysInDrilldowns { get; set; }
/// <summary>
/// Determines collection and pivot that needs to be executed.
/// Returned pivot contains he filter required to show only rows of the original set that
/// made contribution to the number shown in column/>.
/// </summary>
/// <param name="source">Data source</param>
/// <param name="collectionName">Source collection name</param>
/// <param name="displayName">Column name as displayed to drilldown to</param>
/// <param name="displayToRealNameMap">Display name to real column name map</param>
/// <param name="allColumns">All columns currently shown. Names must be resolvable via getValue</param>
/// <param name="current">Currently shown pivot definition</param>
/// <param name="getValue">Get value of any column for the current row</param>
/// <param name="allDataFields">List of all data fields that can potentially be shown using current pivot</param>
/// <param name="allKeyFields">List of all key fields that can potentially be shown using current pivot</param>
/// <returns>Collection name and pivot definition implementing drilldown for supplied field</returns>
public async Task<Tuple<string, PivotDefinition>?> Drilldown(
IPivotTableDataSource source,
string collectionName,
string displayName,
Dictionary<string, string> displayToRealNameMap,
string [] allColumns,
PivotDefinition current,
Func<string, object?> getValue,
IReadOnlySet<string> allDataFields,
IReadOnlySet<string> allKeyFields
)
{
try
{
var fields = GetPrimaryKey2KeyFields(collectionName, getValue, allColumns).ToArray();
if ( fields.Length == GetCollection(collectionName).FieldTypes.Count( x => x.Value.Purpose == PivotFieldPurpose.PrimaryKey2 ) )
{
// all keys already shown. now ony one document can be selected.
// show the detailed single document view instead of report
await ShowDocument( fields! );
return null;
}
var realName = Rename(displayName, displayToRealNameMap);
var pivotTuple = await GetCustomDrilldown( collectionName, realName );
pivotTuple ??= await TryCustomDrilldown(source, collectionName, current, getValue, realName, allColumns);
if ( pivotTuple != null )
return pivotTuple;
// aggregation drilldown
// there is no reason to check this for map/reduce reports as they always contains unique column names
if ( !allDataFields.Contains( realName ) && !allKeyFields.Contains( realName ) )
{
await MessageBoxShow( $"Drilldown is not supported for Column=\"{realName}\" DisplayedAs=\"{displayName}\"" );
return null;
}
// make inverted dictionary
var realToDisplayNameMap = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
foreach ( var item in displayToRealNameMap )
realToDisplayNameMap[item.Value] = item.Key;
pivotTuple = await DrilldownInternal(source, collectionName, getValue, displayName, realName, realToDisplayNameMap, current, allKeyFields);
return pivotTuple;
}
catch (Exception ex)
{
await ShowException(ex);
return null;
}
}
private static string Rename(string name, Dictionary<string, string> map)
{
if ( !map.TryGetValue(name, out var value) || value == null )
value = name;
return value;
}
private async Task<Tuple<string, PivotDefinition>?> DrilldownInternal(
IPivotTableDataSource dataSource,
string collectionName,
Func<string, object?> getValue,
string displayName,
string realName,
Dictionary<string, string> realToDisplayNameMap,
PivotDefinition source,
IReadOnlySet<string> allKeys)
{
try
{
var value = getValue(displayName);
var isString = value is string;
var isDate = value is DateTime;
var isSql = collectionName.StartsWith("BFG: ");
var keyFields = source.KeyFields.ToArray();
var dataFields = source.DataFields.ToArray();
var current = source.Clone();
current.Group = PivotDefinition.CurrentPivotGroup;
var keyFilter = "";
var fieldTypes = GetCollection(collectionName).FieldTypes;
foreach ( var keyColumn in keyFields )
{
var v = getValue( Rename(keyColumn.Replace( ".", " " ), realToDisplayNameMap) );
var val = v == null
? "null"
: $"\"{v}\"";
if ( fieldTypes.TryGetValue( keyColumn, out var desc ) )
{
if ( desc.Type == typeof(DateTime) && v?.GetType() == typeof(DateTime) )
val = isSql ? $"'{v:yyyy-MM-dd}'" : $"ISODate(\"{v:yyyy-MM-ddTHH:mm:ss.fff}Z\")";
else if ( desc.Type != typeof(string) )
val = val.Trim('\"');
}
if ( keyFilter != "" )
keyFilter += GetFilterConcatenationString(collectionName);
//keyFilter += $"{{ \"{keyColumn}\": {val} }}";
keyFilter += await dataSource.GetDrilldownAsync(collectionName, keyColumn, val, true );
}
var nullValue = isString
? "\"\""
: isDate
? "null"
: "0.0";
if ( keyFilter != "" )
keyFilter += GetFilterConcatenationString(collectionName);
keyFilter += await dataSource.GetDrilldownAsync(collectionName, realName, nullValue );
current.DrilldownFilter = keyFilter;
var keys1 = GetCollection(collectionName).GetDrilldownKeyFields(PivotFieldPurpose.PrimaryKey1);
var keys2 = GetCollection(collectionName).GetDrilldownKeyFields(PivotFieldPurpose.PrimaryKey2);
var keys = (keys1.All(x => keyFields.Contains(x)) ? keys2 : keys1 )
.Concat(
ShowAllKeysInDrilldowns
? allKeys
: keyFields )
.Distinct()
.Where( allKeys.Contains )
;
current.KeyFields = keys.ToArray();
current.DataFields = dataFields;
_log.Debug( $"Drilldown Pivot=\"{source.Name}\" Filter:\n{current.Filter} DrilldownFilter:\n{current.DrilldownFilter}");
return Tuple.Create(collectionName, current);
}
catch ( Exception ex )
{
await ShowException( ex );
return null;
}
}
private static string GetFilterConcatenationString(string collectionName) => collectionName.StartsWith("Forge:") ? "," : "\n\tAND ";
private async Task<Tuple<string, PivotDefinition>?> TryCustomDrilldown(
IPivotTableDataSource dataSource,
string collectionName,
PivotDefinition? origPivot,
Func<string, object?> getValue,
string column,
string[] allHeaders)
{
// try to find custom drilldown
var rec = origPivot?.Drilldown?.FirstOrDefault( x => x.ColumnName == column )
?? origPivot?.Drilldown?.FirstOrDefault( x => x.ColumnName == DefaultDrilldownField )
;
if ( rec == null )
return null;
// detect other keys shown
var keyHeaders = GetCollection(collectionName).KeyFields;
var keys = allHeaders
.Where( x => keyHeaders.Contains( x ) )
.Select( x => new KeyValuePair<string, string>(x, getValue( x )?.ToString() ?? "") )
;
var basePivot = GetPivotDefinition( rec.DrilldownPivot, collectionName );
if ( basePivot != null )
{
var pivot = await SameCollectionDrilldown( dataSource, collectionName, origPivot, getValue, column, allHeaders, basePivot, rec, keys );
return Tuple.Create(collectionName, pivot);
}
var path = rec.DrilldownPivot.Split( '/' );
if ( path.Length == 2 )
{
if ( !await MessageBoxShowYesNo(
$"Do you want to start separate instance and run \"{path[1]}\" for collection \"{path[0]}\" in order to drill down to \"{column}\"?",
"Drill down" ) )
{
return null;
}
var tuple = await SeparateCollectionDrilldown( origPivot, getValue, column, allHeaders, keys, path[0], path[1] );
return tuple;
}
await MessageBoxShow( $"DrilldownPivot=\"{rec.DrilldownPivot}\" is not found. Column=\"{column}\"" );
return null;
}
private async Task<Tuple<string, PivotDefinition>?> SeparateCollectionDrilldown(
PivotDefinition? orig,
Func<string, object?> getValue,
string column,
string [] allHeaders,
IEnumerable<KeyValuePair<string, string>> keys,
string destCollection,
string destPivotName
)
{
var destPivot = GetPivotDefinition(destPivotName, destCollection)?.Clone();
var rec = orig?.Drilldown.FirstOrDefault( x => x.ColumnName == column )
?? orig?.Drilldown.FirstOrDefault( x => x.ColumnName == DefaultDrilldownField )
;
if (rec == null || destPivot == null)
{
await MessageBoxShow($"Drilldown is not configured for \"{column}\"");
return null;
}
destPivot.DrilldownFilter = string.Concat(keys.Select(x => $"{{ \"{x.Key}\" : \"{x.Value}\" }}, ")) // other keys
+ PrepareDrilldownCondition(allHeaders, rec.DrilldownCondition, column, getValue); // drilldown condition
return Tuple.Create(destCollection, destPivot);
}
private Task<PivotDefinition> SameCollectionDrilldown(
IPivotTableDataSource dataSource,
string collectionName,
PivotDefinition? origPivot,
Func<string, object?> getValue,
string column,
string [] allHeaders,
PivotDefinition basePivot,
PivotDefinition.DrilldownDef rec,
IEnumerable<KeyValuePair<string, string>> keys
)
{
var drill = basePivot.Clone();
drill.DrilldownFilter = string.Concat(keys.Select(x => $"{{ \"{x.Key}\" : \"{x.Value}\" }}, ")) // other keys
+ PrepareDrilldownCondition(allHeaders, rec.DrilldownCondition, column, getValue); // drilldown condition
drill.Name = PivotDefinition.CurrentPivotName;
drill.Group = PivotDefinition.CurrentPivotGroup;
if ( !string.IsNullOrWhiteSpace( rec.AppendToBeforeGrouping ) )
{
// append to "Before Grouping"
if ( !string.IsNullOrWhiteSpace( drill.BeforeGrouping ) )
drill.BeforeGrouping += ",\n";
drill.BeforeGrouping += PrepareDrilldownCondition( allHeaders, rec.AppendToBeforeGrouping, column, getValue );
}
var primaryKeys1 = GetCollection(collectionName).FieldTypes.Values
.Where( x => x.Purpose == PivotFieldPurpose.PrimaryKey1 )
.Select( x => x.Name )
.ToList();
if ( origPivot?.KeyFields.Intersect( primaryKeys1 ).Count() != primaryKeys1.Count
) // not all PK1 shown => add missing PK1
{
drill.KeyFields = drill.KeyFields.Concat( primaryKeys1.Where( x => !drill.KeyFields.Contains( x ) ) ).ToArray();
}
else
{
// do the same for PK2
var primaryKeys2 = GetCollection(collectionName).FieldTypes.Values
.Where( x => x.Purpose == PivotFieldPurpose.PrimaryKey2 )
.Select( x => x.Name )
.ToList();
if ( origPivot.KeyFields.Intersect( primaryKeys2 ).Count() != primaryKeys2.Count
) // not all PK2 selected => add missing PK1 and PK2
{
drill.KeyFields = drill.KeyFields.Concat( primaryKeys1.Where( x => !drill.KeyFields.Contains( x ) ) )
.ToArray();
drill.KeyFields = drill.KeyFields.Concat( primaryKeys2.Where( x => !drill.KeyFields.Contains( x ) ) )
.ToArray();
}
}
return Task.FromResult(drill);
}
private IEnumerable<KeyValuePair<string, object?>> GetPrimaryKey2KeyFields(
string collectionName,
Func<string, object?> getValue,
string [] allHeaders
) =>
GetCollection(collectionName).FieldTypes
.Where( x => x.Value.Purpose == PivotFieldPurpose.PrimaryKey2 )
.Select( x => x.Key )
.Where( allHeaders.Contains )
.Select( key => new KeyValuePair<string, object?>( key, getValue( key ) ) );
private static string PrepareDrilldownCondition(
string [] allHeaders,
string cond,
string columnName,
Func<string, object?> getValue
)
{
cond = allHeaders
.Aggregate(
cond,
(current, header) => current.Replace($"<{header}>", getValue(header)?.ToString())
);
cond = cond.Replace("<COLNAME>", columnName);
return cond;
}
}