File: System\UriTemplateTable.cs
Project: ndp\cdf\src\WCF\ServiceModel\System.ServiceModel.csproj (System.ServiceModel)
//----------------------------------------------------------------
// Copyright (c) Microsoft Corporation.  All rights reserved.
//----------------------------------------------------------------
 
namespace System
{
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.Collections.Specialized;
    using System.Diagnostics;
    using System.Diagnostics.CodeAnalysis;
    using System.Runtime;
    using System.ServiceModel;
    using System.Runtime.CompilerServices;
 
    // this class is thread-safe
    [TypeForwardedFrom("System.ServiceModel.Web, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35")]
    public class UriTemplateTable
    {
        Uri baseAddress;
        string basePath;
        Dictionary<string, FastPathInfo> fastPathTable; // key is uri.PathAndQuery, fastPathTable may be null
        bool noTemplateHasQueryPart;
        int numSegmentsInBaseAddress;
        Uri originalUncanonicalizedBaseAddress;
        UriTemplateTrieNode rootNode;
        UriTemplatesCollection templates;
        object thisLock;
        bool addTrailingSlashToBaseAddress;
 
        public UriTemplateTable()
            : this(null, null, true)
        {
        }
        public UriTemplateTable(IEnumerable<KeyValuePair<UriTemplate, object>> keyValuePairs)
            : this(null, keyValuePairs, true)
        {
        }
        public UriTemplateTable(Uri baseAddress)
            : this(baseAddress, null, true)
        {
        }
 
        internal UriTemplateTable(Uri baseAddress, bool addTrailingSlashToBaseAddress)
            : this(baseAddress, null, addTrailingSlashToBaseAddress)
        {
        }
 
        public UriTemplateTable(Uri baseAddress, IEnumerable<KeyValuePair<UriTemplate, object>> keyValuePairs)
            : this(baseAddress, keyValuePairs, true)
        {
        }
 
        internal UriTemplateTable(Uri baseAddress, IEnumerable<KeyValuePair<UriTemplate, object>> keyValuePairs, bool addTrailingSlashToBaseAddress)
        {
            if (baseAddress != null && !baseAddress.IsAbsoluteUri)
            {
                throw DiagnosticUtility.ExceptionUtility.ThrowHelperArgument("baseAddress", SR.GetString(
                    SR.UTTMustBeAbsolute));
            }
            
            this.addTrailingSlashToBaseAddress = addTrailingSlashToBaseAddress;
            this.originalUncanonicalizedBaseAddress = baseAddress;
            
            if (keyValuePairs != null)
            {
                this.templates = new UriTemplatesCollection(keyValuePairs);
            }
            else
            {
                this.templates = new UriTemplatesCollection();
            }
 
            this.thisLock = new object();
            this.baseAddress = baseAddress;
            NormalizeBaseAddress();
        }
 
        public Uri BaseAddress
        {
            get
            {
                return this.baseAddress;
            }
            set
            {
                if (value == null)
                {
                    throw DiagnosticUtility.ExceptionUtility.ThrowHelperArgumentNull("value");
                }
                lock (this.thisLock)
                {
                    if (this.IsReadOnly)
                    {
                        throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new InvalidOperationException(
                            SR.GetString(SR.UTTCannotChangeBaseAddress)));
                    }
                    else
                    {
                        if (!value.IsAbsoluteUri)
                        {
                            throw DiagnosticUtility.ExceptionUtility.ThrowHelperArgument("value", SR.GetString(
                                SR.UTTBaseAddressMustBeAbsolute));
                        }
                        else
                        {
                            this.originalUncanonicalizedBaseAddress = value;
                            this.baseAddress = value;
                            NormalizeBaseAddress();
                        }
                    }
                }
            }
        }
 
        public Uri OriginalBaseAddress
        {
            get
            {
                return this.originalUncanonicalizedBaseAddress;
            }
        }
 
        public bool IsReadOnly
        {
            get
            {
                return this.templates.IsFrozen;
            }
        }
        public IList<KeyValuePair<UriTemplate, object>> KeyValuePairs
        {
            get
            {
                return this.templates;
            }
        }
 
        public void MakeReadOnly(bool allowDuplicateEquivalentUriTemplates)
        {
            // idempotent
            lock (this.thisLock)
            {
                if (!this.IsReadOnly)
                {
                    this.templates.Freeze();
                    Validate(allowDuplicateEquivalentUriTemplates);
                    ConstructFastPathTable();
                }
            }
        }
        public Collection<UriTemplateMatch> Match(Uri uri)
        {
            if (uri == null)
            {
                throw DiagnosticUtility.ExceptionUtility.ThrowHelperArgumentNull("uri");
            }
            if (!uri.IsAbsoluteUri)
            {
                return None();
            }
 
            this.MakeReadOnly(true);
 
            // Matching path :
            Collection<String> relativeSegments;
            IList<UriTemplateTableMatchCandidate> candidates;
            if (!FastComputeRelativeSegmentsAndLookup(uri, out relativeSegments, out candidates))
            {
                return None();
            }
            // Matching query :
            NameValueCollection queryParameters = null;
            if (!this.noTemplateHasQueryPart && AtLeastOneCandidateHasQueryPart(candidates))
            {
                Collection<UriTemplateTableMatchCandidate> nextCandidates = new Collection<UriTemplateTableMatchCandidate>();
                Fx.Assert(nextCandidates.Count == 0, "nextCandidates should be empty");
 
                // then deal with query
                queryParameters = UriTemplateHelpers.ParseQueryString(uri.Query);
                bool mustBeEspeciallyInteresting = NoCandidateHasQueryLiteralRequirementsAndThereIsAnEmptyFallback(candidates);
                for (int i = 0; i < candidates.Count; i++)
                {
                    if (UriTemplateHelpers.CanMatchQueryInterestingly(candidates[i].Template, queryParameters, mustBeEspeciallyInteresting))
                    {
                        nextCandidates.Add(candidates[i]);
                    }
                }
                if (nextCandidates.Count > 1)
                {
                    Fx.Assert(AllEquivalent(nextCandidates, 0, nextCandidates.Count), "demux algorithm problem, multiple non-equivalent matches");
                }
 
                if (nextCandidates.Count == 0)
                {
                    for (int i = 0; i < candidates.Count; i++)
                    {
                        if (UriTemplateHelpers.CanMatchQueryTrivially(candidates[i].Template))
                        {
                            nextCandidates.Add(candidates[i]);
                        }
                    }
                }
                if (nextCandidates.Count == 0)
                {
                    return None();
                }
                if (nextCandidates.Count > 1)
                {
                    Fx.Assert(AllEquivalent(nextCandidates, 0, nextCandidates.Count), "demux algorithm problem, multiple non-equivalent matches");
                }
 
                candidates = nextCandidates;
            }
            // Verifying that we have not broken the allowDuplicates settings because of terminal defaults
            //  This situation can be caused when we are hosting ".../" and ".../{foo=xyz}" in the same
            //  table. They are not equivalent; yet they reside together in the same path partially-equivalent
            //  set. If we hit a uri that ends up in that particular end-of-path set, we want to provide the
            //  user only the 'best' match and not both; thus preventing inconsistancy between the MakeReadonly
            //  settings and the matching results. We will assume that the 'best' matches will be the ones with
            //  the smallest number of segments - this will prefer ".../" over ".../{x=1}[/...]".
            if (NotAllCandidatesArePathFullyEquivalent(candidates))
            {
                Collection<UriTemplateTableMatchCandidate> nextCandidates = new Collection<UriTemplateTableMatchCandidate>();
                int minSegmentsCount = -1;
                for (int i = 0; i < candidates.Count; i++)
                {
                    UriTemplateTableMatchCandidate candidate = candidates[i];
                    if (minSegmentsCount == -1)
                    {
                        minSegmentsCount = candidate.Template.segments.Count;
                        nextCandidates.Add(candidate);
                    }
                    else if (candidate.Template.segments.Count < minSegmentsCount)
                    {
                        minSegmentsCount = candidate.Template.segments.Count;
                        nextCandidates.Clear();
                        nextCandidates.Add(candidate);
                    }
                    else if (candidate.Template.segments.Count == minSegmentsCount)
                    {
                        nextCandidates.Add(candidate);
                    }
                }
                Fx.Assert(minSegmentsCount != -1, "At least the first entry in the list should be kept");
                Fx.Assert(nextCandidates.Count >= 1, "At least the first entry in the list should be kept");
                Fx.Assert(nextCandidates[0].Template.segments.Count == minSegmentsCount, "Trivial");
                candidates = nextCandidates;
            }
 
            // Building the actual result
            Collection<UriTemplateMatch> actualResults = new Collection<UriTemplateMatch>();
            for (int i = 0; i < candidates.Count; i++)
            {
                UriTemplateTableMatchCandidate candidate = candidates[i];
                UriTemplateMatch match = candidate.Template.CreateUriTemplateMatch(this.originalUncanonicalizedBaseAddress,
                    uri, candidate.Data, candidate.SegmentsCount, relativeSegments, queryParameters);
                actualResults.Add(match);
            }
            return actualResults;
        }
        public UriTemplateMatch MatchSingle(Uri uri)
        {
            Collection<UriTemplateMatch> c = this.Match(uri);
            if (c.Count == 0)
            {
                return null;
            }
            if (c.Count == 1)
            {
                return c[0];
            }
            throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new UriTemplateMatchException(SR.GetString(
                SR.UTTMultipleMatches)));
        }
 
        [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "This method is called within a Debug assert")]
        static bool AllEquivalent(IList<UriTemplateTableMatchCandidate> list, int a, int b)
        {
            for (int i = a; i < b - 1; ++i)
            {
                if (!list[i].Template.IsPathPartiallyEquivalentAt(list[i + 1].Template, list[i].SegmentsCount))
                {
                    return false;
                }
                if (!list[i].Template.IsQueryEquivalent(list[i + 1].Template))
                {
                    return false;
                }
            }
            return true;
        }
 
        static bool AtLeastOneCandidateHasQueryPart(IList<UriTemplateTableMatchCandidate> candidates)
        {
            for (int i = 0; i < candidates.Count; i++)
            {
                if (!UriTemplateHelpers.CanMatchQueryTrivially(candidates[i].Template))
                {
                    return true;
                }
            }
            return false;
        }
        static bool NoCandidateHasQueryLiteralRequirementsAndThereIsAnEmptyFallback(
            IList<UriTemplateTableMatchCandidate> candidates)
        {
            bool thereIsAmEmptyFallback = false;
            for (int i = 0; i < candidates.Count; i++)
            {
                if (UriTemplateHelpers.HasQueryLiteralRequirements(candidates[i].Template))
                {
                    return false;
                }
                if (candidates[i].Template.queries.Count == 0)
                {
                    thereIsAmEmptyFallback = true;
                }
            }
            return thereIsAmEmptyFallback;
        }
 
        static Collection<UriTemplateMatch> None()
        {
            return new Collection<UriTemplateMatch>();
        }
        static bool NotAllCandidatesArePathFullyEquivalent(IList<UriTemplateTableMatchCandidate> candidates)
        {
            if (candidates.Count <= 1)
            {
                return false;
            }
 
            int segmentsCount = -1;
            for (int i = 0; i < candidates.Count; i++)
            {
                if (segmentsCount == -1)
                {
                    segmentsCount = candidates[i].Template.segments.Count;
                }
                else if (segmentsCount != candidates[i].Template.segments.Count)
                {
                    return true;
                }
            }
            return false;
        }
 
        bool ComputeRelativeSegmentsAndLookup(Uri uri,
            ICollection<string> relativePathSegments, // add to this
            ICollection<UriTemplateTableMatchCandidate> candidates) // matched candidates
        {
            string[] uriSegments = uri.Segments;
            int numRelativeSegments = uriSegments.Length - this.numSegmentsInBaseAddress;
            Fx.Assert(numRelativeSegments >= 0, "bad num segments");
            UriTemplateLiteralPathSegment[] uSegments = new UriTemplateLiteralPathSegment[numRelativeSegments];
            for (int i = 0; i < numRelativeSegments; ++i)
            {
                string seg = uriSegments[i + this.numSegmentsInBaseAddress];
                // compute representation for matching
                UriTemplateLiteralPathSegment lps = UriTemplateLiteralPathSegment.CreateFromWireData(seg);
                uSegments[i] = lps;
                // compute representation to project out into results
                string relPathSeg = Uri.UnescapeDataString(seg);
                if (lps.EndsWithSlash)
                {
                    Fx.Assert(relPathSeg.EndsWith("/", StringComparison.Ordinal), "problem with relative path segment");
                    relPathSeg = relPathSeg.Substring(0, relPathSeg.Length - 1); // trim slash
                }
                relativePathSegments.Add(relPathSeg);
            }
            return rootNode.Match(uSegments, candidates);
        }
        void ConstructFastPathTable()
        {
            this.noTemplateHasQueryPart = true;
            foreach (KeyValuePair<UriTemplate, object> kvp in this.templates)
            {
                UriTemplate ut = kvp.Key;
                if (!UriTemplateHelpers.CanMatchQueryTrivially(ut))
                {
                    this.noTemplateHasQueryPart = false;
                }
                if (ut.HasNoVariables && !ut.HasWildcard)
                {
                    // eligible for fast path
                    if (this.fastPathTable == null)
                    {
                        this.fastPathTable = new Dictionary<string, FastPathInfo>();
                    }
                    Uri uri = ut.BindByPosition(this.originalUncanonicalizedBaseAddress);
                    string uriPath = UriTemplateHelpers.GetUriPath(uri);
                    if (this.fastPathTable.ContainsKey(uriPath))
                    {
                        // nothing to do, we've already seen it
                    }
                    else
                    {
                        FastPathInfo fpInfo = new FastPathInfo();
                        if (ComputeRelativeSegmentsAndLookup(uri, fpInfo.RelativePathSegments,
                            fpInfo.Candidates))
                        {
                            fpInfo.Freeze();
                            this.fastPathTable.Add(uriPath, fpInfo);
                        }
                    }
                }
            }
        }
        // this method checks the literal cache for a match if none, goes through the slower path of cracking the segments
        bool FastComputeRelativeSegmentsAndLookup(Uri uri, out Collection<string> relativePathSegments,
            out IList<UriTemplateTableMatchCandidate> candidates)
        {
            // Consider fast-path and lookup
            // return false if not under base uri
            string uriPath = UriTemplateHelpers.GetUriPath(uri);
            FastPathInfo fpInfo = null;
            if ((this.fastPathTable != null) && this.fastPathTable.TryGetValue(uriPath, out fpInfo))
            {
                relativePathSegments = fpInfo.RelativePathSegments;
                candidates = fpInfo.Candidates;
                VerifyThatFastPathAndSlowPathHaveSameResults(uri, relativePathSegments, candidates);
                return true;
            }
            else
            {
                relativePathSegments = new Collection<string>();
                candidates = new Collection<UriTemplateTableMatchCandidate>();
                return SlowComputeRelativeSegmentsAndLookup(uri, uriPath, relativePathSegments, candidates);
            }
        }
 
        void NormalizeBaseAddress()
        {
            if (this.baseAddress != null)
            {
                // ensure trailing slash on baseAddress, so that IsBaseOf will work later
                UriBuilder ub = new UriBuilder(this.baseAddress);
                if (this.addTrailingSlashToBaseAddress && !ub.Path.EndsWith("/", StringComparison.Ordinal))
                {
                    ub.Path = ub.Path + "/";
                }
                ub.Host = "localhost"; // always normalize to localhost
                ub.Port = -1;
                ub.UserName = null;
                ub.Password = null;
                ub.Path = ub.Path.ToUpperInvariant();
                ub.Scheme = Uri.UriSchemeHttp;
                this.baseAddress = ub.Uri;
                basePath = UriTemplateHelpers.GetUriPath(this.baseAddress);
            }
        }
        bool SlowComputeRelativeSegmentsAndLookup(Uri uri, string uriPath, Collection<string> relativePathSegments,
            ICollection<UriTemplateTableMatchCandidate> candidates)
        {
            // ensure 'under' the base address
            if (uriPath.Length < basePath.Length)
            {
                return false;
            }
 
            if (!uriPath.StartsWith(basePath, StringComparison.OrdinalIgnoreCase))
            {
                return false;
            }
            else
            {
                // uriPath StartsWith basePath, but this check is not enough - basePath 'service1' should not match with uriPath 'service123'
                // make sure that after the match the next character is /, this is to avoid a uriPath of the form /service12/ matching with a basepath of the form /service1
                if (uriPath.Length > basePath.Length && !basePath.EndsWith("/", StringComparison.Ordinal) && uriPath[basePath.Length] != '/')
                {
                    return false;
                }
            }
 
            return ComputeRelativeSegmentsAndLookup(uri, relativePathSegments, candidates);
        }
 
        void Validate(bool allowDuplicateEquivalentUriTemplates)
        {
            if (this.baseAddress == null)
            {
                throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new InvalidOperationException(SR.GetString(
                    SR.UTTBaseAddressNotSet)));
            }
            this.numSegmentsInBaseAddress = this.baseAddress.Segments.Length;
            if (this.templates.Count == 0)
            {
                throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(new InvalidOperationException(SR.GetString(
                    SR.UTTEmptyKeyValuePairs)));
            }
            // build the trie and
            // validate that forall Uri u, at most one UriTemplate is a best match for u
            rootNode = UriTemplateTrieNode.Make(this.templates, allowDuplicateEquivalentUriTemplates);
        }
        [Conditional("DEBUG")]
        void VerifyThatFastPathAndSlowPathHaveSameResults(Uri uri, Collection<string> fastPathRelativePathSegments,
            IList<UriTemplateTableMatchCandidate> fastPathCandidates)
        {
            Collection<string> slowPathRelativePathSegments = new Collection<string>();
            List<UriTemplateTableMatchCandidate> slowPathCandidates = new List<UriTemplateTableMatchCandidate>();
            if (!SlowComputeRelativeSegmentsAndLookup(uri, UriTemplateHelpers.GetUriPath(uri),
                slowPathRelativePathSegments, slowPathCandidates))
            {
                Fx.Assert("fast path yielded a result but slow path yielded no result");
            }
            // compare results
            if (fastPathRelativePathSegments.Count != slowPathRelativePathSegments.Count)
            {
                Fx.Assert("fast path yielded different number of segments from slow path");
            }
            for (int i = 0; i < fastPathRelativePathSegments.Count; ++i)
            {
                if (fastPathRelativePathSegments[i] != slowPathRelativePathSegments[i])
                {
                    Fx.Assert("fast path yielded different segments from slow path");
                }
            }
            if (fastPathCandidates.Count != slowPathCandidates.Count)
            {
                Fx.Assert("fast path yielded different number of candidates from slow path");
            }
            for (int i = 0; i < fastPathCandidates.Count; i++)
            {
                if (!slowPathCandidates.Contains(fastPathCandidates[i]))
                {
                    Fx.Assert("fast path yielded different candidates from slow path");
                }
            }
        }
 
        class FastPathInfo
        {
            FreezableCollection<UriTemplateTableMatchCandidate> candidates;
            FreezableCollection<string> relativePathSegments;
 
            public FastPathInfo()
            {
                this.relativePathSegments = new FreezableCollection<string>();
                this.candidates = new FreezableCollection<UriTemplateTableMatchCandidate>();
            }
            public Collection<UriTemplateTableMatchCandidate> Candidates
            {
                get
                {
                    return this.candidates;
                }
            }
 
            public Collection<string> RelativePathSegments
            {
                get
                {
                    return this.relativePathSegments;
                }
            }
 
            public void Freeze()
            {
                this.relativePathSegments.Freeze();
                this.candidates.Freeze();
            }
        }
 
        class UriTemplatesCollection : FreezableCollection<KeyValuePair<UriTemplate, object>>
        {
            public UriTemplatesCollection()
                : base()
            {
            }
            [SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors", Justification = "This is a private class; virtual methods cannot be overriden")]
            public UriTemplatesCollection(IEnumerable<KeyValuePair<UriTemplate, object>> keyValuePairs)
                : base()
            {
                foreach (KeyValuePair<UriTemplate, object> kvp in keyValuePairs)
                {
                    ThrowIfInvalid(kvp.Key, "keyValuePairs");
                    base.Add(kvp);
                }
            }
 
            protected override void InsertItem(int index, KeyValuePair<UriTemplate, object> item)
            {
                ThrowIfInvalid(item.Key, "item");
                base.InsertItem(index, item);
            }
            protected override void SetItem(int index, KeyValuePair<UriTemplate, object> item)
            {
                ThrowIfInvalid(item.Key, "item");
                base.SetItem(index, item);
            }
 
            static void ThrowIfInvalid(UriTemplate template, string argName)
            {
                if (template == null)
                {
                    throw DiagnosticUtility.ExceptionUtility.ThrowHelperArgument(argName,
                        SR.GetString(SR.UTTNullTemplateKey));
                }
                if (template.IgnoreTrailingSlash)
                {
                    throw DiagnosticUtility.ExceptionUtility.ThrowHelperArgument(argName,
                        SR.GetString(SR.UTTInvalidTemplateKey, template));
                }
            }
        }
    }
}