// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Management.Automation; using System.Management.Automation.Internal; namespace Microsoft.PowerShell.Commands.Internal.Format { internal sealed class TableViewGenerator : ViewGenerator { // tableBody to use for this instance of the ViewGenerator; private TableControlBody _tableBody; internal override void Initialize(TerminatingErrorContext terminatingErrorContext, PSPropertyExpressionFactory mshExpressionFactory, TypeInfoDataBase db, ViewDefinition view, FormattingCommandLineParameters formatParameters) { base.Initialize(terminatingErrorContext, mshExpressionFactory, db, view, formatParameters); if ((this.dataBaseInfo != null) && (this.dataBaseInfo.view != null)) { _tableBody = (TableControlBody)this.dataBaseInfo.view.mainControl; } } internal override void Initialize(TerminatingErrorContext errorContext, PSPropertyExpressionFactory expressionFactory, PSObject so, TypeInfoDataBase db, FormattingCommandLineParameters parameters) { base.Initialize(errorContext, expressionFactory, so, db, parameters); if ((this.dataBaseInfo != null) && (this.dataBaseInfo.view != null)) { _tableBody = (TableControlBody)this.dataBaseInfo.view.mainControl; } List rawMshParameterList = null; if (parameters != null) rawMshParameterList = parameters.mshParameterList; // check if we received properties from the command line if (rawMshParameterList is not null && rawMshParameterList.Count > 0) { this.activeAssociationList = AssociationManager.ExpandTableParameters(rawMshParameterList, so); } else { // we did not get any properties: // try to get properties from the default property set of the object this.activeAssociationList = AssociationManager.ExpandDefaultPropertySet(so, this.expressionFactory); if (this.activeAssociationList.Count > 0) { // we got a valid set of properties from the default property set..add computername for // remoteobjects (if available) if (PSObjectHelper.ShouldShowComputerNameProperty(so)) { activeAssociationList.Add(new MshResolvedExpressionParameterAssociation(null, new PSPropertyExpression(RemotingConstants.ComputerNameNoteProperty))); } } else { // we failed to get anything from the default property set this.activeAssociationList = AssociationManager.ExpandAll(so); if (this.activeAssociationList.Count > 0) { // Remove PSComputerName and PSShowComputerName from the display as needed. AssociationManager.HandleComputerNameProperties(so, activeAssociationList); FilterActiveAssociationList(); } else { // we were unable to retrieve any properties, so we leave an empty list this.activeAssociationList = new List(); return; } } } ApplyExcludePropertyFilter(); } /// /// Let the view prepare itself for RemoteObjects. This will add "ComputerName" to the /// table columns. /// /// internal override void PrepareForRemoteObjects(PSObject so) { Diagnostics.Assert(so != null, "so cannot be null"); // make sure computername property exists. Diagnostics.Assert(so.Properties[RemotingConstants.ComputerNameNoteProperty] != null, "PrepareForRemoteObjects cannot be called when the object does not contain ComputerName property."); if ((dataBaseInfo != null) && (dataBaseInfo.view != null) && (dataBaseInfo.view.mainControl != null)) { // dont change the original format definition in the database..just make a copy and work // with the copy _tableBody = (TableControlBody)this.dataBaseInfo.view.mainControl.Copy(); TableRowItemDefinition cnRowDefinition = new TableRowItemDefinition(); PropertyTokenBase propToken = new FieldPropertyToken(); propToken.expression = new ExpressionToken(RemotingConstants.ComputerNameNoteProperty, false); cnRowDefinition.formatTokenList.Add(propToken); _tableBody.defaultDefinition.rowItemDefinitionList.Add(cnRowDefinition); // add header only if there are other header definitions if (_tableBody.header.columnHeaderDefinitionList.Count > 0) { TableColumnHeaderDefinition cnHeaderDefinition = new TableColumnHeaderDefinition(); cnHeaderDefinition.label = new TextToken(); cnHeaderDefinition.label.text = RemotingConstants.ComputerNameNoteProperty; _tableBody.header.columnHeaderDefinitionList.Add(cnHeaderDefinition); } } } internal override FormatStartData GenerateStartData(PSObject so) { FormatStartData startFormat = base.GenerateStartData(so); if (this.dataBaseInfo.view != null) startFormat.shapeInfo = GenerateTableHeaderInfoFromDataBaseInfo(so); else startFormat.shapeInfo = GenerateTableHeaderInfoFromProperties(so); return startFormat; } /// /// Method to filter resolved expressions as per table view needs. /// For v1.0, table view supports only 10 properties. /// /// This method filters and updates "activeAssociationList" instance property. /// /// None. /// This method updates "activeAssociationList" instance property. private void FilterActiveAssociationList() { // we got a valid set of properties from the default property set // make sure we do not have too many properties // NOTE: this is an arbitrary number, chosen to be a sensitive default const int nMax = 10; if (activeAssociationList.Count > nMax) { List tmp = this.activeAssociationList; this.activeAssociationList = new List(); for (int k = 0; k < nMax; k++) this.activeAssociationList.Add(tmp[k]); } return; } private TableHeaderInfo GenerateTableHeaderInfoFromDataBaseInfo(PSObject so) { TableHeaderInfo thi = new TableHeaderInfo(); bool dummy; List activeRowItemDefinitionList = GetActiveTableRowDefinition(_tableBody, so, out dummy); thi.hideHeader = this.HideHeaders; thi.repeatHeader = this.RepeatHeader; int col = 0; foreach (TableRowItemDefinition rowItem in activeRowItemDefinitionList) { TableColumnInfo ci = new TableColumnInfo(); TableColumnHeaderDefinition colHeader = null; if (_tableBody.header.columnHeaderDefinitionList.Count > 0) colHeader = _tableBody.header.columnHeaderDefinitionList[col]; if (colHeader != null) { ci.width = colHeader.width; ci.alignment = colHeader.alignment; if (colHeader.label != null) { if (colHeader.label.text != string.Empty) { ci.HeaderMatchesProperty = so.Properties[colHeader.label.text] is not null; } ci.label = this.dataBaseInfo.db.displayResourceManagerCache.GetTextTokenString(colHeader.label); } } if (ci.alignment == TextAlignment.Undefined) { ci.alignment = rowItem.alignment; } if (ci.label == null) { FormatToken token = null; if (rowItem.formatTokenList.Count > 0) token = rowItem.formatTokenList[0]; if (token != null) { if (token is FieldPropertyToken fpt) { ci.label = fpt.expression.expressionValue; } else if (token is TextToken tt) { ci.label = this.dataBaseInfo.db.displayResourceManagerCache.GetTextTokenString(tt); } } else { ci.label = string.Empty; } } thi.tableColumnInfoList.Add(ci); col++; } return thi; } private TableHeaderInfo GenerateTableHeaderInfoFromProperties(PSObject so) { TableHeaderInfo thi = new TableHeaderInfo(); thi.hideHeader = this.HideHeaders; thi.repeatHeader = this.RepeatHeader; for (int k = 0; k < this.activeAssociationList.Count; k++) { MshResolvedExpressionParameterAssociation a = this.activeAssociationList[k]; TableColumnInfo ci = new TableColumnInfo(); // set the label of the column if (a.OriginatingParameter != null) { object key = a.OriginatingParameter.GetEntry(FormatParameterDefinitionKeys.LabelEntryKey); if (key != AutomationNull.Value) ci.propertyName = (string)key; } ci.propertyName ??= this.activeAssociationList[k].ResolvedExpression.ToString(); // set the width of the table if (a.OriginatingParameter != null) { object key = a.OriginatingParameter.GetEntry(FormatParameterDefinitionKeys.WidthEntryKey); if (key != AutomationNull.Value) ci.width = (int)key; else { ci.width = 0; // let Column Width Manager decide the width } } else { ci.width = 0; // let Column Width Manager decide the width } // set the alignment if (a.OriginatingParameter != null) { object key = a.OriginatingParameter.GetEntry(FormatParameterDefinitionKeys.AlignmentEntryKey); if (key != AutomationNull.Value) ci.alignment = (int)key; else ci.alignment = ComputeDefaultAlignment(so, a.ResolvedExpression); } else { ci.alignment = ComputeDefaultAlignment(so, a.ResolvedExpression); } thi.tableColumnInfoList.Add(ci); } return thi; } private bool HideHeaders { get { // first check command line, it takes the precedence if (this.parameters != null && this.parameters.shapeParameters != null) { TableSpecificParameters tableSpecific = (TableSpecificParameters)this.parameters.shapeParameters; if (tableSpecific != null && tableSpecific.hideHeaders.HasValue) { return tableSpecific.hideHeaders.Value; } } // if we have a view, get the value out of it if (this.dataBaseInfo.view != null) { return _tableBody.header.hideHeader; } return false; } } private bool RepeatHeaders { get { if (this.parameters != null) { return this.parameters.repeatHeader; } return false; } } private static int ComputeDefaultAlignment(PSObject so, PSPropertyExpression ex) { List rList = ex.GetValues(so); if ((rList.Count == 0) || (rList[0].Exception != null)) return TextAlignment.Left; object val = rList[0].Result; if (val == null) return TextAlignment.Left; PSObject soVal = PSObject.AsPSObject(val); var typeNames = soVal.InternalTypeNames; if (string.Equals(PSObjectHelper.PSObjectIsOfExactType(typeNames), "System.String", StringComparison.OrdinalIgnoreCase)) return TextAlignment.Left; if (DefaultScalarTypes.IsTypeInList(typeNames)) return TextAlignment.Right; return TextAlignment.Left; } internal override FormatEntryData GeneratePayload(PSObject so, int enumerationLimit) { FormatEntryData fed = new FormatEntryData(); TableRowEntry tre; if (this.dataBaseInfo.view != null) { tre = GenerateTableRowEntryFromDataBaseInfo(so, enumerationLimit); } else { tre = GenerateTableRowEntryFromFromProperties(so, enumerationLimit); // get the global setting for multiline tre.multiLine = this.dataBaseInfo.db.defaultSettingsSection.MultilineTables; } fed.formatEntryInfo = tre; // override from command line, if there if (this.parameters != null && this.parameters.shapeParameters != null) { TableSpecificParameters tableSpecific = (TableSpecificParameters)this.parameters.shapeParameters; if (tableSpecific != null && tableSpecific.multiLine.HasValue) { tre.multiLine = tableSpecific.multiLine.Value; } } return fed; } private List GetActiveTableRowDefinition(TableControlBody tableBody, PSObject so, out bool multiLine) { multiLine = tableBody.defaultDefinition.multiLine; if (tableBody.optionalDefinitionList.Count == 0) { // we do not have any override, use default return tableBody.defaultDefinition.rowItemDefinitionList; } // see if we have an override that matches TableRowDefinition matchingRowDefinition = null; var typeNames = so.InternalTypeNames; TypeMatch match = new TypeMatch(expressionFactory, this.dataBaseInfo.db, typeNames); foreach (TableRowDefinition x in tableBody.optionalDefinitionList) { if (match.PerfectMatch(new TypeMatchItem(x, x.appliesTo))) { matchingRowDefinition = x; break; } } matchingRowDefinition ??= match.BestMatch as TableRowDefinition; if (matchingRowDefinition == null) { Collection typesWithoutPrefix = Deserializer.MaskDeserializationPrefix(typeNames); if (typesWithoutPrefix != null) { match = new TypeMatch(expressionFactory, this.dataBaseInfo.db, typesWithoutPrefix); foreach (TableRowDefinition x in tableBody.optionalDefinitionList) { if (match.PerfectMatch(new TypeMatchItem(x, x.appliesTo))) { matchingRowDefinition = x; break; } } matchingRowDefinition ??= match.BestMatch as TableRowDefinition; } } if (matchingRowDefinition == null) { // no matching override, use default return tableBody.defaultDefinition.rowItemDefinitionList; } // the overriding row definition takes the precedence if (matchingRowDefinition.multiLine) multiLine = matchingRowDefinition.multiLine; // we have an override, we need to compute the merge of the active cells List activeRowItemDefinitionList = new List(); int col = 0; foreach (TableRowItemDefinition rowItem in matchingRowDefinition.rowItemDefinitionList) { // check if the row is an override or not if (rowItem.formatTokenList.Count == 0) { // it's a place holder, use the default activeRowItemDefinitionList.Add(tableBody.defaultDefinition.rowItemDefinitionList[col]); } else { // use the override activeRowItemDefinitionList.Add(rowItem); } col++; } return activeRowItemDefinitionList; } private TableRowEntry GenerateTableRowEntryFromDataBaseInfo(PSObject so, int enumerationLimit) { TableRowEntry tre = new TableRowEntry(); List activeRowItemDefinitionList = GetActiveTableRowDefinition(_tableBody, so, out tre.multiLine); foreach (TableRowItemDefinition rowItem in activeRowItemDefinitionList) { FormatPropertyField fpf = GenerateFormatPropertyField(rowItem.formatTokenList, so, enumerationLimit); // get the alignment from the row entry // NOTE: if it's not set, the alignment sent with the header will prevail fpf.alignment = rowItem.alignment; tre.formatPropertyFieldList.Add(fpf); } return tre; } private TableRowEntry GenerateTableRowEntryFromFromProperties(PSObject so, int enumerationLimit) { TableRowEntry tre = new TableRowEntry(); for (int k = 0; k < this.activeAssociationList.Count; k++) { FormatPropertyField fpf = new FormatPropertyField(); FieldFormattingDirective directive = null; if (activeAssociationList[k].OriginatingParameter != null) { directive = activeAssociationList[k].OriginatingParameter.GetEntry(FormatParameterDefinitionKeys.FormatStringEntryKey) as FieldFormattingDirective; } if (directive is null) { directive = new FieldFormattingDirective(); directive.isTable = true; } fpf.propertyValue = this.GetExpressionDisplayValue(so, enumerationLimit, this.activeAssociationList[k].ResolvedExpression, directive); tre.formatPropertyFieldList.Add(fpf); } return tre; } } }