Support Object Explorer FindNodes request (#589)

This commit is contained in:
Matt Irvine
2018-03-15 10:47:52 -07:00
committed by GitHub
parent d36efb578c
commit 365fe2282e
8 changed files with 584 additions and 33 deletions

View File

@@ -29,6 +29,7 @@
<ProjectReference Include="../Microsoft.SqlTools.Credentials/Microsoft.SqlTools.Credentials.csproj" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="ObjectExplorer\SmoModel\TreeNodeDefinition.xml" />
<EmbeddedResource Include="Localization\sr.resx" />
<None Include="Localization\sr.strings" />
</ItemGroup>

View File

@@ -0,0 +1,55 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using System.Collections.Generic;
using Microsoft.SqlTools.Hosting.Protocol.Contracts;
using Microsoft.SqlTools.ServiceLayer.Connection.Contracts;
namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer.Contracts
{
/// <summary>
/// Information returned from a <see cref="FindNodesRequest"/>.
/// </summary>
public class FindNodesResponse
{
/// <summary>
/// Information describing the matching nodes in the tree
/// </summary>
public List<NodeInfo> Nodes { get; set; }
}
/// <summary>
/// Parameters to the <see cref="FindNodesRequest"/>.
/// </summary>
public class FindNodesParams
{
/// <summary>
/// The Id returned from a <see cref="CreateSessionRequest"/>. This
/// is used to disambiguate between different trees.
/// </summary>
public string SessionId { get; set; }
public string Type { get; set; }
public string Schema { get; set; }
public string Name { get; set; }
public string Database { get; set; }
public List<string> ParentObjectNames { get; set; }
}
/// <summary>
/// TODO
/// </summary>
public class FindNodesRequest
{
public static readonly
RequestType<FindNodesParams, FindNodesResponse> Type =
RequestType<FindNodesParams, FindNodesResponse>.Create("objectexplorer/findnodes");
}
}

View File

@@ -181,7 +181,7 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer.Nodes
nodePath = path;
}
public TreeNode FindNodeByPath(string path)
public TreeNode FindNodeByPath(string path, bool refreshChildren = false)
{
TreeNode nodeForPath = ObjectExplorerUtils.FindNode(this, node =>
{
@@ -189,7 +189,7 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer.Nodes
}, nodeToFilter =>
{
return path.StartsWith(nodeToFilter.GetNodePath());
});
}, refreshChildren);
return nodeForPath;
}

View File

@@ -47,7 +47,6 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer
private ConnectedBindingQueue bindingQueue = new ConnectedBindingQueue(needsMetadata: false);
private string connectionName = "ObjectExplorer";
/// <summary>
/// This timeout limits the amount of time that object explorer tasks can take to complete
/// </summary>
@@ -60,6 +59,7 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer
{
sessionMap = new ConcurrentDictionary<string, ObjectExplorerSession>();
applicableNodeChildFactories = new Lazy<Dictionary<string, HashSet<ChildFactory>>>(() => PopulateFactories());
NodePathGenerator.Initialize();
}
internal ConnectedBindingQueue ConnectedBindingQueue
@@ -136,6 +136,7 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer
serviceHost.SetRequestHandler(ExpandRequest.Type, HandleExpandRequest);
serviceHost.SetRequestHandler(RefreshRequest.Type, HandleRefreshRequest);
serviceHost.SetRequestHandler(CloseSessionRequest.Type, HandleCloseSessionRequest);
serviceHost.SetRequestHandler(FindNodesRequest.Type, HandleFindNodesRequest);
WorkspaceService<SqlToolsSettings> workspaceService = WorkspaceService;
if (workspaceService != null)
{
@@ -293,6 +294,16 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer
await HandleRequestAsync(closeSession, context, "HandleCloseSessionRequest");
}
internal async Task HandleFindNodesRequest(FindNodesParams findNodesParams, RequestContext<FindNodesResponse> context)
{
var foundNodes = FindNodes(findNodesParams.SessionId, findNodesParams.Type, findNodesParams.Schema, findNodesParams.Name, findNodesParams.Database, findNodesParams.ParentObjectNames);
if (foundNodes == null)
{
foundNodes = new List<TreeNode>();
}
await context.SendResult(new FindNodesResponse { Nodes = foundNodes.Select(node => node.ToNodeInfo()).ToList() });
}
internal void CloseSession(string uri)
{
ObjectExplorerSession session;
@@ -689,6 +700,37 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer
applicableFactories.Add(factory);
}
/// <summary>
/// Find all tree nodes matching the given node information
/// </summary>
/// <param name="sessionId">The ID of the object explorer session to find nodes for</param>
/// <param name="typeName">The requested node type</param>
/// <param name="schema">The schema for the requested object, or null if not applicable</param>
/// <param name="name">The name of the requested object</param>
/// <param name="databaseName">The name of the database containing the requested object, or null if not applicable</param>
/// <param name="parentNames">The name of any other parent objects in the object explorer tree, from highest in the tree to lowest</param>
/// <returns>A list of nodes matching the given information, or an empty list if no nodes match</returns>
public List<TreeNode> FindNodes(string sessionId, string typeName, string schema, string name, string databaseName, List<string> parentNames = null)
{
var nodes = new List<TreeNode>();
var oeSession = sessionMap.GetValueOrDefault(sessionId);
if (oeSession == null)
{
return nodes;
}
var outputPaths = NodePathGenerator.FindNodePaths(oeSession, typeName, schema, name, databaseName, parentNames);
foreach (var outputPath in outputPaths)
{
var treeNode = oeSession.Root.FindNodeByPath(outputPath, true);
if (treeNode != null)
{
nodes.Add(treeNode);
}
}
return nodes;
}
internal class ObjectExplorerTaskResult
{
public bool IsCompleted { get; set; }

View File

@@ -49,7 +49,7 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer
/// determines whether to stop going further up the tree</param>
/// <param name="filter">Predicate function to filter the children when traversing</param>
/// <returns>A Tree Node that matches the condition</returns>
public static TreeNode FindNode(TreeNode node, Predicate<TreeNode> condition, Predicate<TreeNode> filter)
public static TreeNode FindNode(TreeNode node, Predicate<TreeNode> condition, Predicate<TreeNode> filter, bool refreshChildren = false)
{
if(node == null)
{
@@ -60,11 +60,12 @@ namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer
{
return node;
}
foreach (var child in node.GetChildren())
var children = refreshChildren && !node.IsAlwaysLeaf ? node.Refresh() : node.GetChildren();
foreach (var child in children)
{
if (filter != null && filter(child))
{
TreeNode childNode = FindNode(child, condition, filter);
TreeNode childNode = FindNode(child, condition, filter, refreshChildren);
if (childNode != null)
{
return childNode;

View File

@@ -0,0 +1,268 @@
//
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
//
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml.Serialization;
namespace Microsoft.SqlTools.ServiceLayer.ObjectExplorer.SmoModel
{
public class NodePathGenerator
{
private static ServerExplorerTree TreeRoot { get; set; }
private static Dictionary<string, HashSet<Node>> NodeTypeDictionary { get; set; }
internal static void Initialize()
{
if (TreeRoot != null)
{
return;
}
var assembly = typeof(ObjectExplorerService).Assembly;
var resource = assembly.GetManifestResourceStream("Microsoft.SqlTools.ServiceLayer.ObjectExplorer.SmoModel.TreeNodeDefinition.xml");
var serializer = new XmlSerializer(typeof(ServerExplorerTree));
NodeTypeDictionary = new Dictionary<string, HashSet<Node>>();
using (var reader = new StreamReader(resource))
{
TreeRoot = (ServerExplorerTree)serializer.Deserialize(reader);
}
foreach (var node in TreeRoot.Nodes)
{
var containedType = node.ContainedType();
if (containedType != null && node.Label() != string.Empty)
{
if (!NodeTypeDictionary.ContainsKey(containedType))
{
NodeTypeDictionary.Add(containedType, new HashSet<Node>());
}
NodeTypeDictionary.GetValueOrDefault(containedType).Add(node);
}
}
var serverNode = TreeRoot.Nodes.FirstOrDefault(node => node.Name == "Server");
var serverSet = new HashSet<Node>();
serverSet.Add(serverNode);
NodeTypeDictionary.Add("Server", serverSet);
}
internal static HashSet<string> FindNodePaths(ObjectExplorerService.ObjectExplorerSession objectExplorerSession, string typeName, string schema, string name, string databaseName, List<string> parentNames = null)
{
if (TreeRoot == null)
{
Initialize();
}
var returnSet = new HashSet<string>();
var matchingNodes = NodeTypeDictionary.GetValueOrDefault(typeName);
if (matchingNodes == null)
{
return returnSet;
}
var path = name;
if (schema != null)
{
path = schema + "." + path;
}
if (path == null)
{
path = "";
}
foreach (var matchingNode in matchingNodes)
{
var paths = GenerateNodePath(objectExplorerSession, matchingNode, databaseName, parentNames, path);
foreach (var newPath in paths)
{
returnSet.Add(newPath);
}
}
return returnSet;
}
private static HashSet<string> GenerateNodePath(ObjectExplorerService.ObjectExplorerSession objectExplorerSession, Node currentNode, string databaseName, List<string> parentNames, string path)
{
if (parentNames != null)
{
parentNames = parentNames.ToList();
}
if (currentNode.Name == "Server" || (currentNode.Name == "Database" && objectExplorerSession.Root.NodeType == "Database"))
{
var serverRoot = objectExplorerSession.Root;
if (objectExplorerSession.Root.NodeType == "Database")
{
serverRoot = objectExplorerSession.Root.Parent;
path = objectExplorerSession.Root.NodeValue + (path.Length > 0 ? ("/" + path) : "");
}
path = serverRoot.NodeValue + (path.Length > 0 ? ("/" + path) : "");
var returnSet = new HashSet<string>();
returnSet.Add(path);
return returnSet;
}
var currentLabel = currentNode.Label();
if (currentLabel != string.Empty)
{
path = currentLabel + "/" + path;
var returnSet = new HashSet<string>();
foreach (var parent in currentNode.ParentNodes())
{
var paths = GenerateNodePath(objectExplorerSession, parent, databaseName, parentNames, path);
foreach (var newPath in paths)
{
returnSet.Add(newPath);
}
}
return returnSet;
}
else
{
var returnSet = new HashSet<string>();
if (currentNode.ContainedType() == "Database")
{
path = databaseName + "/" + path;
}
else if (parentNames != null && parentNames.Count > 0)
{
var parentName = parentNames.Last();
parentNames.RemoveAt(parentNames.Count - 1);
path = parentName + "/" + path;
}
else
{
return returnSet;
}
foreach (var parentNode in currentNode.ParentNodes())
{
var newPaths = GenerateNodePath(objectExplorerSession, parentNode, databaseName, parentNames, path);
foreach (var newPath in newPaths)
{
returnSet.Add(newPath);
}
}
return returnSet;
}
}
[XmlRoot("ServerExplorerTree")]
public class ServerExplorerTree
{
[XmlElement("Node", typeof(Node))]
public List<Node> Nodes { get; set; }
public Node GetNode(string name)
{
foreach (var node in this.Nodes)
{
if (node.Name == name)
{
return node;
}
}
return null;
}
}
public class Node
{
[XmlAttribute]
public string Name { get; set; }
[XmlAttribute]
public string LocLabel { get; set; }
[XmlAttribute]
public string TreeNode { get; set; }
[XmlAttribute]
public string NodeType { get; set; }
[XmlElement("Child", typeof(Child))]
public List<Child> Children { get; set; }
public HashSet<Node> ChildFolders()
{
var childSet = new HashSet<Node>();
foreach (var child in this.Children)
{
var node = TreeRoot.GetNode(child.Name);
if (node != null)
{
childSet.Add(node);
}
}
return childSet;
}
public string ContainedType()
{
if (this.TreeNode != null)
{
return this.TreeNode.Replace("TreeNode", "");
}
else if (this.NodeType != null)
{
return this.NodeType;
}
return null;
}
public Node ContainedObject()
{
var containedType = this.ContainedType();
if (containedType == null)
{
return null;
}
var containedNode = TreeRoot.GetNode(containedType);
if (containedNode == this)
{
return null;
}
return containedNode;
}
public string Label()
{
if (this.LocLabel.StartsWith("SR."))
{
return SR.Keys.GetString(this.LocLabel.Remove(0, 3));
}
return string.Empty;
}
public HashSet<Node> ParentNodes()
{
var parentNodes = new HashSet<Node>();
foreach (var node in TreeRoot.Nodes)
{
if (this != node && (node.ContainedType() == this.Name || node.Children.Any(child => child.Name == this.Name)))
{
parentNodes.Add(node);
}
}
return parentNodes;
}
}
public class Child
{
[XmlAttribute]
public string Name { get; set; }
}
}
}

View File

@@ -0,0 +1,164 @@
//
// 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.Data.SqlClient;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.SqlTools.Hosting.Protocol;
using Microsoft.SqlTools.ServiceLayer.Connection;
using Microsoft.SqlTools.ServiceLayer.Connection.Contracts;
using Microsoft.SqlTools.ServiceLayer.ObjectExplorer;
using Microsoft.SqlTools.ServiceLayer.ObjectExplorer.Contracts;
using Microsoft.SqlTools.ServiceLayer.ObjectExplorer.Nodes;
using Microsoft.SqlTools.ServiceLayer.ObjectExplorer.SmoModel;
using Microsoft.SqlTools.ServiceLayer.UnitTests.Utility;
using Moq;
using Xunit;
using Microsoft.SqlTools.ServiceLayer.LanguageServices;
using Microsoft.SqlServer.Management.Common;
using Microsoft.SqlTools.ServiceLayer.Test.Common.RequestContextMocking;
namespace Microsoft.SqlTools.ServiceLayer.UnitTests.ObjectExplorer
{
public class NodePathGeneratorTests
{
private ObjectExplorerService.ObjectExplorerSession serverSession;
private ObjectExplorerService.ObjectExplorerSession databaseSession;
private const string serverName = "testServer";
private const string databaseName = "testDatabase";
public NodePathGeneratorTests()
{
var serverRoot = new TreeNode
{
NodeType = "Server",
NodeValue = serverName
};
serverSession = new ObjectExplorerService.ObjectExplorerSession("serverUri", serverRoot, null, null);
var databaseRoot = new TreeNode
{
NodeType = "Database",
NodeValue = databaseName,
Parent = serverRoot
};
databaseSession = new ObjectExplorerService.ObjectExplorerSession("databaseUri", databaseRoot, null, null);
}
[Fact]
public void FindCorrectPathsForTableWithServerRoot()
{
var paths = NodePathGenerator.FindNodePaths(serverSession, "Table", "testSchema", "testTable", databaseName);
var expectedPaths = new List<string>
{
"testServer/Databases/testDatabase/Tables/testSchema.testTable",
"testServer/Databases/System Databases/testDatabase/Tables/testSchema.testTable",
"testServer/Databases/testDatabase/Tables/System Tables/testSchema.testTable",
"testServer/Databases/System Databases/testDatabase/Tables/System Tables/testSchema.testTable"
};
Assert.Equal(expectedPaths.Count, paths.Count);
foreach (var expectedPath in expectedPaths)
{
Assert.True(paths.Contains(expectedPath));
}
}
[Fact]
public void FindCorrectPathsForTableWithDatabaseRoot()
{
var paths = NodePathGenerator.FindNodePaths(databaseSession, "Table", "testSchema", "testTable", null);
var expectedPaths = new List<string>
{
"testServer/testDatabase/Tables/testSchema.testTable",
"testServer/testDatabase/Tables/System Tables/testSchema.testTable"
};
Assert.Equal(expectedPaths.Count, paths.Count);
foreach (var expectedPath in expectedPaths)
{
Assert.True(paths.Contains(expectedPath));
}
}
[Fact]
public void FindCorrectPathsForColumnWithServerRoot()
{
var paths = NodePathGenerator.FindNodePaths(serverSession, "Column", null, "testColumn", databaseName, new List<string> { "testSchema.testTable" });
var expectedPaths = new List<string>
{
"testServer/Databases/testDatabase/Tables/testSchema.testTable/Columns/testColumn",
"testServer/Databases/System Databases/testDatabase/Tables/testSchema.testTable/Columns/testColumn",
"testServer/Databases/testDatabase/Tables/System Tables/testSchema.testTable/Columns/testColumn",
"testServer/Databases/System Databases/testDatabase/Tables/System Tables/testSchema.testTable/Columns/testColumn",
"testServer/Databases/testDatabase/Views/testSchema.testTable/Columns/testColumn",
"testServer/Databases/System Databases/testDatabase/Views/testSchema.testTable/Columns/testColumn",
"testServer/Databases/testDatabase/Views/System Views/testSchema.testTable/Columns/testColumn",
"testServer/Databases/System Databases/testDatabase/Views/System Views/testSchema.testTable/Columns/testColumn"
};
Assert.Equal(expectedPaths.Count, paths.Count);
foreach (var expectedPath in expectedPaths)
{
Assert.True(paths.Contains(expectedPath));
}
}
[Fact]
public void FindCorrectPathsForColumnWithDatabaseRoot()
{
var paths = NodePathGenerator.FindNodePaths(databaseSession, "Column", null, "testColumn", databaseName, new List<string> { "testSchema.testTable" });
var expectedPaths = new List<string>
{
"testServer/testDatabase/Tables/testSchema.testTable/Columns/testColumn",
"testServer/testDatabase/Tables/System Tables/testSchema.testTable/Columns/testColumn",
"testServer/testDatabase/Views/testSchema.testTable/Columns/testColumn",
"testServer/testDatabase/Views/System Views/testSchema.testTable/Columns/testColumn"
};
Assert.Equal(expectedPaths.Count, paths.Count);
foreach (var expectedPath in expectedPaths)
{
Assert.True(paths.Contains(expectedPath));
}
}
[Fact]
public void FindCorrectPathsForDatabase()
{
var paths = NodePathGenerator.FindNodePaths(serverSession, "Database", null, databaseName, null);
var expectedPaths = new List<string>
{
"testServer/Databases/testDatabase",
"testServer/Databases/System Databases/testDatabase"
};
Assert.Equal(expectedPaths.Count, paths.Count);
foreach (var expectedPath in expectedPaths)
{
Assert.True(paths.Contains(expectedPath));
}
}
[Fact]
public void FindPathForInvalidTypeReturnsEmpty()
{
var serverPaths = NodePathGenerator.FindNodePaths(serverSession, "WrongType", "testSchema", "testTable", databaseName);
Assert.Equal(0, serverPaths.Count);
}
[Fact]
public void FindPathMissingParentReturnsEmpty()
{
var serverPaths = NodePathGenerator.FindNodePaths(serverSession, "Column", "testSchema", "testColumn", databaseName);
Assert.Equal(0, serverPaths.Count);
}
}
}

View File

@@ -243,6 +243,26 @@ namespace Microsoft.SqlTools.ServiceLayer.UnitTests.ObjectExplorer
connectionServiceMock.Verify(c => c.Disconnect(It.IsAny<DisconnectParams>()));
}
[Fact]
public async Task FindNodesReturnsMatchingNode()
{
var session = await CreateSession();
var foundNodes = service.FindNodes(session.SessionId, "Server", null, null, null);
Assert.Equal(1, foundNodes.Count);
Assert.Equal("Server", foundNodes[0].NodeType);
Assert.Equal(session.RootNode.NodePath, foundNodes[0].ToNodeInfo().NodePath);
}
[Fact]
public async Task FindNodesReturnsEmptyListForNoMatch()
{
var session = await CreateSession();
var foundNodes = service.FindNodes(session.SessionId, "Table", "testSchema", "testTable", "testDatabase");
Assert.Equal(0, foundNodes.Count);
}
private async Task<SessionCreatedParameters> CreateSession()
{
SessionCreatedParameters sessionResult = null;