//---------------------------------------------------------------- // 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 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> 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> keyValuePairs) : this(baseAddress, keyValuePairs, true) { } internal UriTemplateTable(Uri baseAddress, IEnumerable> 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> 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 Match(Uri uri) { if (uri == null) { throw DiagnosticUtility.ExceptionUtility.ThrowHelperArgumentNull("uri"); } if (!uri.IsAbsoluteUri) { return None(); } this.MakeReadOnly(true); // Matching path : Collection relativeSegments; IList candidates; if (!FastComputeRelativeSegmentsAndLookup(uri, out relativeSegments, out candidates)) { return None(); } // Matching query : NameValueCollection queryParameters = null; if (!this.noTemplateHasQueryPart && AtLeastOneCandidateHasQueryPart(candidates)) { Collection nextCandidates = new Collection(); 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 nextCandidates = new Collection(); 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 actualResults = new Collection(); 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 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 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 candidates) { for (int i = 0; i < candidates.Count; i++) { if (!UriTemplateHelpers.CanMatchQueryTrivially(candidates[i].Template)) { return true; } } return false; } static bool NoCandidateHasQueryLiteralRequirementsAndThereIsAnEmptyFallback( IList 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 None() { return new Collection(); } static bool NotAllCandidatesArePathFullyEquivalent(IList 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 relativePathSegments, // add to this ICollection 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 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(); } 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 relativePathSegments, out IList 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(); candidates = new Collection(); 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 relativePathSegments, ICollection 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 fastPathRelativePathSegments, IList fastPathCandidates) { Collection slowPathRelativePathSegments = new Collection(); List slowPathCandidates = new List(); 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 candidates; FreezableCollection relativePathSegments; public FastPathInfo() { this.relativePathSegments = new FreezableCollection(); this.candidates = new FreezableCollection(); } public Collection Candidates { get { return this.candidates; } } public Collection RelativePathSegments { get { return this.relativePathSegments; } } public void Freeze() { this.relativePathSegments.Freeze(); this.candidates.Freeze(); } } class UriTemplatesCollection : FreezableCollection> { public UriTemplatesCollection() : base() { } [SuppressMessage("Microsoft.Usage", "CA2214:DoNotCallOverridableMethodsInConstructors", Justification = "This is a private class; virtual methods cannot be overriden")] public UriTemplatesCollection(IEnumerable> keyValuePairs) : base() { foreach (KeyValuePair kvp in keyValuePairs) { ThrowIfInvalid(kvp.Key, "keyValuePairs"); base.Add(kvp); } } protected override void InsertItem(int index, KeyValuePair item) { ThrowIfInvalid(item.Key, "item"); base.InsertItem(index, item); } protected override void SetItem(int index, KeyValuePair 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)); } } } } }