// Copyright (c) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.Linq;
namespace System.Web
{
///
/// This is a container for prefix values. It normalizes all the values into dotted-form and then stores
/// them in a sorted array. All queries for prefixes are also normalized to dotted-form, and searches
/// for ContainsPrefix are done with a binary search.
///
internal class PrefixContainer
{
private readonly ICollection _originalValues;
private readonly string[] _sortedValues;
internal PrefixContainer(ICollection values)
{
if (values == null)
{
throw new ArgumentNullException("values");
}
_originalValues = values;
_sortedValues = values.Where(val => val != null).ToArray();
Array.Sort(_sortedValues, StringComparer.OrdinalIgnoreCase);
}
internal bool ContainsPrefix(string prefix)
{
if (prefix == null)
{
throw new ArgumentNullException("prefix");
}
if (prefix.Length == 0)
{
return _sortedValues.Length > 0; // only match empty string when we have some value
}
return Array.BinarySearch(_sortedValues, prefix, new PrefixComparer(prefix)) > -1;
}
// Given "foo.bar", "foo.hello", "something.other", foo[abc].baz and asking for prefix "foo" will return:
// - "bar"/"foo.bar"
// - "hello"/"foo.hello"
// - "abc"/"foo[abc]"
internal IDictionary GetKeysFromPrefix(string prefix)
{
IDictionary result = new Dictionary(StringComparer.OrdinalIgnoreCase);
foreach (var entry in _originalValues)
{
if (entry != null)
{
if (entry.Length == prefix.Length)
{
// No key in this entry
continue;
}
if (prefix.Length == 0)
{
GetKeyFromEmptyPrefix(entry, result);
}
else if (entry.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
{
GetKeyFromNonEmptyPrefix(prefix, entry, result);
}
}
}
return result;
}
private static void GetKeyFromEmptyPrefix(string entry, IDictionary results)
{
string key;
int dotPosition = entry.IndexOf('.');
int bracketPosition = entry.IndexOf('[');
int delimiterPosition = -1;
if (dotPosition == -1)
{
if (bracketPosition != -1)
{
delimiterPosition = bracketPosition;
}
}
else
{
if (bracketPosition == -1)
{
delimiterPosition = dotPosition;
}
else
{
delimiterPosition = Math.Min(dotPosition, bracketPosition);
}
}
key = delimiterPosition == -1 ? entry : entry.Substring(0, delimiterPosition);
results[key] = key;
}
private static void GetKeyFromNonEmptyPrefix(string prefix, string entry, IDictionary results)
{
string key = null;
string fullName = null;
int keyPosition = prefix.Length + 1;
switch (entry[prefix.Length])
{
case '.':
int dotPosition = entry.IndexOf('.', keyPosition);
if (dotPosition == -1)
{
dotPosition = entry.Length;
}
key = entry.Substring(keyPosition, dotPosition - keyPosition);
fullName = entry.Substring(0, dotPosition);
break;
case '[':
int bracketPosition = entry.IndexOf(']', keyPosition);
if (bracketPosition == -1)
{
// Malformed for dictionary
return;
}
key = entry.Substring(keyPosition, bracketPosition - keyPosition);
fullName = entry.Substring(0, bracketPosition + 1);
break;
default:
return;
}
if (!results.ContainsKey(key))
{
results.Add(key, fullName);
}
}
internal static bool IsPrefixMatch(string prefix, string testString)
{
if (testString == null)
{
return false;
}
if (prefix.Length == 0)
{
return true; // shortcut - non-null testString matches empty prefix
}
if (prefix.Length > testString.Length)
{
return false; // not long enough
}
if (!testString.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
{
return false; // prefix doesn't match
}
if (testString.Length == prefix.Length)
{
return true; // exact match
}
// invariant: testString.Length > prefix.Length
switch (testString[prefix.Length])
{
case '.':
case '[':
return true; // known delimiters
default:
return false; // not known delimiter
}
}
private class PrefixComparer : IComparer
{
private string _prefix;
public PrefixComparer(string prefix)
{
_prefix = prefix;
}
public int Compare(string x, string y)
{
string testString = Object.ReferenceEquals(x, _prefix) ? y : x;
if (IsPrefixMatch(_prefix, testString))
{
return 0;
}
return StringComparer.OrdinalIgnoreCase.Compare(x, y);
}
}
}
}