// // Copyright (c) Microsoft. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. // using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; using System.Globalization; using System.Threading; using System.Linq; using Microsoft.Kusto.ServiceLayer.DataSource; using Microsoft.Kusto.ServiceLayer.DataSource.Metadata; using Microsoft.Kusto.ServiceLayer.ObjectExplorer.Contracts; using Microsoft.Kusto.ServiceLayer.ObjectExplorer.DataSourceModel; using Microsoft.Kusto.ServiceLayer.Utility; using Microsoft.SqlTools.Utility; namespace Microsoft.Kusto.ServiceLayer.ObjectExplorer.Nodes { /// /// Base class for elements in the object explorer tree. Provides common methods for tree navigation /// and other core functionality /// public class TreeNode : IComparable { private NodeObservableCollection children = new NodeObservableCollection(); private TreeNode parent; private string nodePath; private string label; private string nodePathName; private const char PathPartSeperator = '/'; /// /// Object metadata /// public DataSourceObjectMetadata ObjectMetadata { get; set; } /// /// The DataSource this tree node is representing /// public IDataSource DataSource { get; set; } /// /// Constructor with DataSource and DataSourceObjectMetadata /// /// /// protected TreeNode(IDataSource dataSource, DataSourceObjectMetadata objectMetadata) { DataSource = dataSource; ObjectMetadata = objectMetadata; NodeValue = objectMetadata.PrettyName; } private object buildingMetadataLock = new object(); /// /// Event which tells if MetadataProvider is built fully or not /// public object BuildingMetadataLock { get { return this.buildingMetadataLock; } } /// /// Value describing this node /// public string NodeValue { get; set; } /// /// The name of this object as included in its node path /// public string NodePathName { get { if (string.IsNullOrEmpty(nodePathName)) { return NodeValue; } return nodePathName; } set { nodePathName = value; } } /// /// The type of the node - for example Server, Database, Folder, Table /// public string NodeType { get; set; } /// // True if the node includes system object /// public bool IsSystemObject { get; set; } /// /// Enum defining the type of the node - for example Server, Database, Folder, Table /// public NodeTypes NodeTypeId { get; set; } /// /// Node Sub type - for example a key can have type as "Key" and sub type as "PrimaryKey" /// public string NodeSubType { get; set; } /// /// Error message returned from the engine for a object explorer node failure reason, if any. /// public string ErrorMessage { get; set; } /// /// Node status - for example login can be disabled/enabled /// public string NodeStatus { get; set; } /// /// Label to display to the user, describing this node. /// If not explicitly set this will fall back to the but /// for many nodes such as the server, the display label will be different /// to the value. /// protected string Label { get { if(label == null) { return NodeValue; } return label; } set { label = value; } } /// /// Is this a leaf node (in which case no children can be generated) or /// is it expandable? /// public bool IsAlwaysLeaf { get; set; } /// /// Message to show if this Node is in an error state. This indicates /// that children could be retrieved /// public string ErrorStateMessage { get; set; } /// /// Parent of this node /// public TreeNode Parent { get { return parent; } set { parent = value; // Reset the node path since it's no longer valid nodePath = null; } } /// /// Path identifying this node: for example a table will be at ["server", "database", "tables", "tableName"]. /// This enables rapid navigation of the tree without the need for a global registry of elements. /// The path functions as a unique ID and is used to disambiguate the node when sending requests for expansion. /// A common ID is needed since processes do not share address space and need a unique identifier /// public string GetNodePath() { if (nodePath == null) { GenerateNodePath(); } return nodePath; } private void GenerateNodePath() { string path = ""; ObjectExplorerUtils.VisitChildAndParents(this, node => { if (string.IsNullOrEmpty(node.NodeValue)) { // Hit a node with no NodeValue. This indicates we need to stop traversing return false; } // Otherwise add this value to the beginning of the path and keep iterating up path = string.Format(CultureInfo.InvariantCulture, "{0}{1}{2}", node.NodePathName, string.IsNullOrEmpty(path) ? "" : PathPartSeperator.ToString(), path); return true; }); nodePath = path; } public TreeNode FindNodeByPath(string path, bool expandIfNeeded = false) { TreeNode nodeForPath = ObjectExplorerUtils.FindNode(this, node => { return node.GetNodePath() == path; }, nodeToFilter => { return path.StartsWith(nodeToFilter.GetNodePath()); }, expandIfNeeded); return nodeForPath; } /// /// Converts to a object for serialization with just the relevant properties /// needed to identify the node /// /// public NodeInfo ToNodeInfo() { return new NodeInfo() { IsLeaf = this.IsAlwaysLeaf, Label = this.Label, NodePath = this.GetNodePath(), NodeType = this.NodeType, Metadata = this.ObjectMetadata, NodeStatus = this.NodeStatus, NodeSubType = this.NodeSubType, ErrorMessage = this.ErrorMessage }; } /// /// Expands this node and returns its children /// /// Children as an IList. This is the raw children collection, not a copy public IList Expand(string name, CancellationToken cancellationToken) { // TODO consider why solution explorer has separate Children and Items options if (children.IsInitialized) { return children; } PopulateChildren(false, name, cancellationToken); return children; } /// /// Expands this node and returns its children /// /// Children as an IList. This is the raw children collection, not a copy public IList Expand(CancellationToken cancellationToken) { return Expand(null, cancellationToken); } /// /// Refresh this node and returns its children /// /// Children as an IList. This is the raw children collection, not a copy public virtual IList Refresh(CancellationToken cancellationToken) { // TODO consider why solution explorer has separate Children and Items options PopulateChildren(true, null, cancellationToken); return children; } /// /// Gets a readonly view of the currently defined children for this node. /// This does not expand the node at all /// Since the tree needs to keep track of parent relationships, directly /// adding to the list is not supported. /// /// containing all children for this node public IList GetChildren() { return new ReadOnlyCollection(children); } /// /// Optional context to help with lookup of children /// public virtual object GetContext() { return null; } /// /// Helper method to convert context to expected format /// /// Type to convert to /// context as expected type of null if it doesn't match public T GetContextAs() where T : class { return GetContext() as T; } public T ParentAs() where T : TreeNode { return Parent as T; } protected void PopulateChildren(bool refresh, string name, CancellationToken cancellationToken) { Logger.Write(TraceEventType.Verbose, string.Format(CultureInfo.InvariantCulture, "Populating oe node :{0}", this.GetNodePath())); Debug.Assert(IsAlwaysLeaf == false); QueryContext context = this.GetContextAs(); if (children.IsPopulating || context == null) { return; } children.Clear(); BeginChildrenInit(); try { ErrorMessage = null; cancellationToken.ThrowIfCancellationRequested(); IEnumerable items = ExpandChildren(this, refresh, name, true, cancellationToken); if (items != null) { foreach (TreeNode item in items) { children.Add(item); item.Parent = this; } } } catch (Exception ex) { string error = string.Format(CultureInfo.InvariantCulture, "Failed populating oe children. error:{0} inner:{1} stacktrace:{2}", ex.Message, ex.InnerException != null ? ex.InnerException.Message : "", ex.StackTrace); Logger.Write(TraceEventType.Error, error); ErrorMessage = ex.Message; } finally { EndChildrenInit(); } } protected IEnumerable ExpandChildren(TreeNode parent, bool refresh, string name, bool includeSystemObjects, CancellationToken cancellationToken) { try { return OnExpandPopulateNonFolders(parent, refresh, name, cancellationToken); } catch (Exception ex) { string error = string.Format(CultureInfo.InvariantCulture, "Failed expanding oe children. parent:{0} error:{1} inner:{2} stacktrace:{3}", parent != null ? parent.GetNodePath() : "", ex.Message, ex.InnerException != null ? ex.InnerException.Message : "", ex.StackTrace); Logger.Write(TraceEventType.Error, error); throw ex; } } /// /// Populates any non-folder nodes such as specific items in the tree. /// /// List to which nodes should be added /// Parent the nodes are being added to private List OnExpandPopulateNonFolders(TreeNode parent, bool refresh, string name, CancellationToken cancellationToken) { Logger.Write(TraceEventType.Verbose, string.Format(CultureInfo.InvariantCulture, "child factory parent :{0}", parent.GetNodePath())); cancellationToken.ThrowIfCancellationRequested(); try { var objectMetadataList = Enumerable.Empty(); if (parent.DataSource != null) { if (refresh) { parent.DataSource.Refresh(parent.ObjectMetadata); } objectMetadataList = parent.DataSource.GetChildObjects(parent.ObjectMetadata); } List allChildren = new List(); foreach (var objectMetadata in objectMetadataList) { cancellationToken.ThrowIfCancellationRequested(); if (objectMetadata == null) { Logger.Write(TraceEventType.Error, "kustoMetadata should not be null"); } TreeNode childNode = CreateChild(parent, objectMetadata); if (childNode != null) { allChildren.Add(childNode); } } return allChildren; } catch (Exception ex) { string error = string.Format(CultureInfo.InvariantCulture, "Failed getting child objects. parent:{0} error:{1} inner:{2} stacktrace:{3}", parent != null ? parent.GetNodePath() : "", ex.Message, ex.InnerException != null ? ex.InnerException.Message : "", ex.StackTrace); Logger.Write(TraceEventType.Error, error); throw ex; } } /// /// The glue between the DataSource and the Object Explorer models. Creates the right tree node for each data source type /// /// /// /// /// private TreeNode CreateChild(TreeNode parent, DataSourceObjectMetadata childMetadata) { ValidationUtils.IsNotNull(parent, nameof(parent)); ValidationUtils.IsNotNull(childMetadata, nameof(childMetadata)); switch(childMetadata.MetadataType) { case DataSourceMetadataType.Database: return new DataSourceTreeNode(parent.DataSource, childMetadata) { Parent = parent as ServerNode, NodeType = "Database", NodeTypeId = NodeTypes.Database }; case DataSourceMetadataType.Table: return new DataSourceTreeNode(parent.DataSource, childMetadata) { NodeType = "Table", NodeTypeId = NodeTypes.Table }; case DataSourceMetadataType.Column: return new DataSourceTreeNode(parent.DataSource, childMetadata) { IsAlwaysLeaf = true, NodeType = "Column", SortPriority = DataSourceTreeNode.NextSortPriority }; case DataSourceMetadataType.Folder: return new DataSourceTreeNode(parent.DataSource, childMetadata) { Parent = parent, NodeType = "Folder", NodeTypeId = NodeTypes.Folder }; case DataSourceMetadataType.Function: return new DataSourceTreeNode(parent.DataSource, childMetadata) { parent = parent, NodeType = "Function", NodeTypeId = NodeTypes.Functions, IsAlwaysLeaf = true, }; default: throw new ArgumentException($"Unexpected type {childMetadata.MetadataType}."); } } public void BeginChildrenInit() { children.BeginInit(); } public void EndChildrenInit() { children.EndInit(); // TODO consider use of deferred children and if it's necessary // children.EndInit(this, ref deferredChildren); } /// /// Sort Priority to help when ordering elements in the tree /// public int? SortPriority { get; set; } protected virtual int CompareSamePriorities(TreeNode thisItem, TreeNode otherItem) { return string.Compare(thisItem.NodeValue, otherItem.NodeValue, StringComparison.OrdinalIgnoreCase); } public int CompareTo(TreeNode other) { if (!this.SortPriority.HasValue && !other.SortPriority.HasValue) { return CompareSamePriorities(this, other); } if (this.SortPriority.HasValue && !other.SortPriority.HasValue) { return -1; // this is above other } if (!this.SortPriority.HasValue) { return 1; // this is below other } // Both have sort priority int priDiff = this.SortPriority.Value - other.SortPriority.Value; if (priDiff < 0) return -1; // this is below other if (priDiff == 0) return 0; return 1; } } }