// // 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 Microsoft.SqlTools.ServiceLayer.Metadata.Contracts; using Microsoft.SqlTools.ServiceLayer.ObjectExplorer.Contracts; using Microsoft.SqlTools.ServiceLayer.ObjectExplorer.SmoModel; using Microsoft.SqlTools.ServiceLayer.Utility; using Microsoft.SqlTools.Utility; namespace Microsoft.SqlTools.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; public const char PathPartSeperator = '/'; /// /// Constructor with no required inputs /// public TreeNode() { } /// /// Constructor that accepts a label to identify the node /// /// Label identifying the node public TreeNode(string value) { // We intentionally do not valid this being null or empty since // some nodes may need to set it NodeValue = value; } 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; } } /// /// Object metadata for smo objects /// public ObjectMetadata ObjectMetadata { get; set; } /// /// 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. /// public 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, ObjectType = this.NodeTypeId.ToString() }; } /// /// 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, string? accessToken = null) { // TODO consider why solution explorer has separate Children and Items options if (children.IsInitialized) { return children; } PopulateChildren(false, name, cancellationToken, accessToken); 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, string? accessToken = null) { return Expand(null, cancellationToken, accessToken); } /// /// 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, string? accessToken = null) { // TODO consider why solution explorer has separate Children and Items options PopulateChildren(true, null, cancellationToken, accessToken); 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); } /// /// Adds a child to the list of children under this node /// /// public void AddChild(TreeNode newChild) { Validate.IsNotNull(nameof(newChild), newChild); children.Add(newChild); newChild.Parent = this; } /// /// 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 virtual void PopulateChildren(bool refresh, string name, CancellationToken cancellationToken, string? accessToken = null) { Logger.Write(TraceEventType.Verbose, string.Format(CultureInfo.InvariantCulture, "Populating oe node :{0}", this.GetNodePath())); Debug.Assert(IsAlwaysLeaf == false); SmoQueryContext context = this.GetContextAs(); bool includeSystemObjects = context != null && context.Database != null ? DatabaseUtils.IsSystemDatabaseConnection(context.Database.Name) : true; if (children.IsPopulating || context == null) { return; } children.Clear(); BeginChildrenInit(); // Update access token for future queries context.UpdateAccessToken(accessToken); try { ErrorMessage = null; IEnumerable childFactories = context.GetObjectExplorerService().GetApplicableChildFactories(this); if (childFactories != null) { foreach (var factory in childFactories) { cancellationToken.ThrowIfCancellationRequested(); try { Logger.Verbose($"Begin populate children for {this.GetNodePath()} using {factory.GetType()} factory"); IEnumerable items = factory.Expand(this, refresh, name, includeSystemObjects, cancellationToken); Logger.Verbose($"End populate children for {this.GetNodePath()} using {factory.GetType()} factory"); 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; } } } } 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(); } } 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); } // Higher SortPriority = lower in the list. A couple nodes are defined with SortPriority of Int16.MaxValue // so they're placed at the bottom of the node list (Dropped Ledger Tables and Dropped Ledger Views folders) // Individual objects, like tables and views, don't have a SortPriority defined, so their values need // to be resolved. If a node doesn't have a SortPriority, set it to the second-highest value. int thisPriority = this.SortPriority ?? Int32.MaxValue - 1; int otherPriority = other.SortPriority ?? Int32.MaxValue - 1; // diff > 0 == this below other // diff < 0 == other below this return thisPriority - otherPriority; } } }