//
// 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.Linq;
using Microsoft.SqlTools.ServiceLayer.Scripting.Contracts;
using Microsoft.SqlTools.Utility;
namespace Microsoft.SqlTools.ServiceLayer.Scripting
{
///
/// Implements matching logic to filter scripting objects based on an
/// include/exclude criteria.
///
///
/// First, objects are included by the include filter. Then, objects are removed by
/// the exclude filter. Matches are made by comparing case insensitive strings for the
/// ScriptingObject Type, Schema, and Name properties. Wildcards '*' are supported for
/// the ScriptingObject Schema and Name properties. Matching on ScriptingObject Type
/// property must be an exact match.
///
/// Examples:
///
/// Include ScriptingObject { Type = null, Schema = "dbo", Name = null }
/// -> matches all objects in the dbo schema.
///
/// Include ScriptingObject { Type = "Table", Schema = "dbo", Name = null }
/// -> matches all tables in the dbo schema.
///
/// Include ScriptingObject { Type = "Table", Schema = null, Name = "Emp*" }
/// -> matches all table names that start with "Emp"
///
/// Include ScriptingObject { Type = "View", Schema = null, Name = "Emp*" }
/// Include ScriptingObject { Type = "Table", Schema = null, Name = "Emp*" }
/// -> matches all table and views with names that start with "Emp"
///
/// Include ScriptingObject { Type = "Table", Schema = null, Name = null }
/// Exclude ScriptingObject { Type = null, Schema = "HumanResources", Name = null }
/// -> matches all tables except tables in the "HumanResources" schema
///
///
public static class ScriptingObjectMatcher
{
private const string Wildcard = "*";
///
/// Given a collection of candidate scripting objects, filters the items that match
/// based on the passed include and exclude criteria.
///
/// The include object criteria.
/// The exclude object criteria.
/// The include schema filter.
/// The exclude schema filter.
/// The include type filter.
/// The exclude type filter.
/// The candidate object to filter.
/// The matching scripting objects.
public static IEnumerable Match(
ScriptingObject includeCriteria,
ScriptingObject excludeCriteria,
string includeSchemas,
string excludeSchemas,
string includeTypes,
string excludeTypes,
IEnumerable candidates)
{
return Match(
includeCriteria == null ? new ScriptingObject[0] : new[] { includeCriteria },
excludeCriteria == null ? new ScriptingObject[0] : new[] { excludeCriteria },
includeSchemas == null ? new List(): new List { includeSchemas },
excludeSchemas == null ? new List(): new List { excludeSchemas },
includeTypes == null ? new List(): new List { includeTypes },
excludeTypes == null ? new List(): new List { excludeTypes },
candidates);
}
///
/// Given a collection of candidate scripting objects, filters the items that match
/// based on the passed include and exclude criteria.
///
/// The collection of include object criteria items.
/// The collection of exclude object criteria items.
/// The collection of include schema items.
/// The collection of exclude schema items.
/// The collection of include type items.
/// The collection of exclude type items.
/// The candidate object to filter.
/// The matching scripting objects.
public static IEnumerable Match(
IEnumerable includeCriteria,
IEnumerable excludeCriteria,
IEnumerable includeSchemas,
IEnumerable excludeSchemas,
IEnumerable includeTypes,
IEnumerable excludeTypes,
IEnumerable candidates)
{
Validate.IsNotNull("candidates", candidates);
IEnumerable matchedObjects = new List();
if (includeCriteria != null && includeCriteria.Any())
{
foreach (ScriptingObject scriptingObjectCriteria in includeCriteria)
{
IEnumerable matches = MatchCriteria(scriptingObjectCriteria, candidates);
matchedObjects = matchedObjects.Union(matches);
}
}
else
{
matchedObjects = candidates;
}
if (excludeCriteria != null && excludeCriteria.Any())
{
foreach (ScriptingObject scriptingObjectCriteria in excludeCriteria)
{
IEnumerable matches = MatchCriteria(scriptingObjectCriteria, matchedObjects);
matchedObjects = matchedObjects.Except(matches);
}
}
// Apply additional filters if included.
matchedObjects = ExcludeSchemaAndOrType(excludeSchemas, excludeTypes, matchedObjects);
matchedObjects = IncludeSchemaAndOrType(includeSchemas, includeTypes, matchedObjects);
return matchedObjects;
}
private static IEnumerable ExcludeSchemaAndOrType(IEnumerable excludeSchemas, IEnumerable excludeTypes,
IEnumerable candidates)
{
// Given a list of candidates, we remove any objects that match the excluded schema and/or type.
IEnumerable remainingObjects = candidates;
IEnumerable matches = null;
if (excludeSchemas != null && excludeSchemas.Any())
{
foreach (string exclude_schema in excludeSchemas)
{
matches = MatchCriteria(exclude_schema, (candidate) => { return candidate.Schema; }, candidates);
remainingObjects = remainingObjects.Except(matches);
}
}
if (excludeTypes != null && excludeTypes.Any())
{
foreach (string exclude_type in excludeTypes)
{
matches = remainingObjects.Where(o => string.Equals(exclude_type, o.Type, StringComparison.OrdinalIgnoreCase));
remainingObjects = remainingObjects.Except(matches);
}
}
return remainingObjects;
}
private static IEnumerable IncludeSchemaAndOrType(IEnumerable includeSchemas, IEnumerable includeTypes,
IEnumerable candidates)
{
// Given a list of candidates, we return a new list of scripting objects that match
// the schema and/or type filter.
IEnumerable matchedSchema = new List();
IEnumerable matchedType = new List();
IEnumerable matchedObjects = new List();
IEnumerable matches = null;
if (includeSchemas != null && includeSchemas.Any())
{
foreach (string include_schema in includeSchemas)
{
matches = MatchCriteria(include_schema, (candidate) => { return candidate.Schema; }, candidates);
matchedSchema = matchedSchema.Union(matches);
}
matchedObjects = matchedSchema;
}
else
{
matchedObjects = candidates;
}
if (includeTypes != null && includeTypes.Any())
{
foreach (string include_type in includeTypes)
{
matches = matchedObjects.Where(o => string.Equals(include_type, o.Type, StringComparison.OrdinalIgnoreCase));
matchedType = matchedType.Union(matches);
}
matchedObjects = matchedType;
}
return matchedObjects;
}
private static IEnumerable MatchCriteria(ScriptingObject criteria, IEnumerable candidates)
{
Validate.IsNotNull("criteria", criteria);
Validate.IsNotNull("candidates", candidates);
IEnumerable matchedObjects = candidates;
if (!string.IsNullOrWhiteSpace(criteria.Type))
{
matchedObjects = matchedObjects.Where(o => string.Equals(criteria.Type, o.Type, StringComparison.OrdinalIgnoreCase));
}
matchedObjects = MatchCriteria(criteria.Schema, (candidate) => { return candidate.Schema; }, matchedObjects);
matchedObjects = MatchCriteria(criteria.Name, (candidate) => { return candidate.Name; }, matchedObjects);
return matchedObjects;
}
private static IEnumerable MatchCriteria(string property, Func propertySelector, IEnumerable candidates)
{
IEnumerable matchedObjects = candidates;
if (!string.IsNullOrWhiteSpace(property))
{
if (property.Equals(Wildcard, StringComparison.OrdinalIgnoreCase))
{
// Don't filter any objects
}
if (property.EndsWith(Wildcard, StringComparison.OrdinalIgnoreCase))
{
matchedObjects = candidates.Where(
o =>
propertySelector(o) != null &&
propertySelector(o).StartsWith(
propertySelector(o).Substring(0, propertySelector(o).Length - 1),
StringComparison.OrdinalIgnoreCase));
}
else
{
matchedObjects = matchedObjects.Where(o => string.Equals(property, propertySelector(o), StringComparison.OrdinalIgnoreCase));
}
}
return matchedObjects;
}
}
}