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