//
// 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;
}
}
}