// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.AspNet.Http; using Microsoft.AspNet.Routing.Logging; using Microsoft.AspNet.Routing.Logging.Internal; using Microsoft.Framework.DependencyInjection; using Microsoft.Framework.Logging; namespace Microsoft.AspNet.Routing.Template { public class TemplateRoute : INamedRouter { private readonly IReadOnlyDictionary _constraints; private readonly IReadOnlyDictionary _dataTokens; private readonly IReadOnlyDictionary _defaults; private readonly IRouter _target; private readonly RouteTemplate _parsedTemplate; private readonly string _routeTemplate; private readonly TemplateMatcher _matcher; private readonly TemplateBinder _binder; private ILogger _logger; private ILogger _constraintLogger; public TemplateRoute( [NotNull] IRouter target, string routeTemplate, IInlineConstraintResolver inlineConstraintResolver) : this(target, routeTemplate, defaults: null, constraints: null, dataTokens: null, inlineConstraintResolver: inlineConstraintResolver) { } public TemplateRoute([NotNull] IRouter target, string routeTemplate, IDictionary defaults, IDictionary constraints, IDictionary dataTokens, IInlineConstraintResolver inlineConstraintResolver) : this(target, null, routeTemplate, defaults, constraints, dataTokens, inlineConstraintResolver) { } public TemplateRoute([NotNull] IRouter target, string routeName, string routeTemplate, IDictionary defaults, IDictionary constraints, IDictionary dataTokens, IInlineConstraintResolver inlineConstraintResolver) { _target = target; _routeTemplate = routeTemplate ?? string.Empty; Name = routeName; _dataTokens = dataTokens == null ? RouteValueDictionary.Empty : new RouteValueDictionary(dataTokens); // Data we parse from the template will be used to fill in the rest of the constraints or // defaults. The parser will throw for invalid routes. _parsedTemplate = TemplateParser.Parse(RouteTemplate); _constraints = GetConstraints(inlineConstraintResolver, RouteTemplate, _parsedTemplate, constraints); _defaults = GetDefaults(_parsedTemplate, defaults); _matcher = new TemplateMatcher(_parsedTemplate, Defaults); _binder = new TemplateBinder(_parsedTemplate, Defaults); } public string Name { get; private set; } public IReadOnlyDictionary Defaults { get { return _defaults; } } public IReadOnlyDictionary DataTokens { get { return _dataTokens; } } public string RouteTemplate { get { return _routeTemplate; } } public IReadOnlyDictionary Constraints { get { return _constraints; } } public async virtual Task RouteAsync([NotNull] RouteContext context) { EnsureLoggers(context.HttpContext); using (_logger.BeginScope("TemplateRoute.RouteAsync")) { var requestPath = context.HttpContext.Request.Path.Value; if (!string.IsNullOrEmpty(requestPath) && requestPath[0] == '/') { requestPath = requestPath.Substring(1); } var values = _matcher.Match(requestPath); if (values == null) { if (_logger.IsEnabled(LogLevel.Verbose)) { _logger.WriteValues(CreateRouteAsyncValues( requestPath, context.RouteData.Values, matchedValues: false, matchedConstraints: false, handled: context.IsHandled)); } // If we got back a null value set, that means the URI did not match return; } var oldRouteData = context.RouteData; var newRouteData = new RouteData(oldRouteData); MergeValues(newRouteData.DataTokens, _dataTokens); newRouteData.Routers.Add(_target); MergeValues(newRouteData.Values, values); if (!RouteConstraintMatcher.Match( Constraints, newRouteData.Values, context.HttpContext, this, RouteDirection.IncomingRequest, _constraintLogger)) { if (_logger.IsEnabled(LogLevel.Verbose)) { _logger.WriteValues(CreateRouteAsyncValues( requestPath, newRouteData.Values, matchedValues: true, matchedConstraints: false, handled: context.IsHandled)); } return; } try { context.RouteData = newRouteData; await _target.RouteAsync(context); if (_logger.IsEnabled(LogLevel.Verbose)) { _logger.WriteValues(CreateRouteAsyncValues( requestPath, newRouteData.Values, matchedValues: true, matchedConstraints: true, handled: context.IsHandled)); } } finally { // Restore the original values to prevent polluting the route data. if (!context.IsHandled) { context.RouteData = oldRouteData; } } } } public virtual string GetVirtualPath(VirtualPathContext context) { var values = _binder.GetValues(context.AmbientValues, context.Values); if (values == null) { // We're missing one of the required values for this route. return null; } EnsureLoggers(context.Context); if (!RouteConstraintMatcher.Match(Constraints, values.CombinedValues, context.Context, this, RouteDirection.UrlGeneration, _constraintLogger)) { return null; } // Validate that the target can accept these values. var childContext = CreateChildVirtualPathContext(context, values.AcceptedValues); var path = _target.GetVirtualPath(childContext); if (path != null) { // If the target generates a value then that can short circuit. context.IsBound = true; return path; } else if (!childContext.IsBound) { // The target has rejected these values. return null; } path = _binder.BindValues(values.AcceptedValues); if (path != null) { context.IsBound = true; } return path; } private VirtualPathContext CreateChildVirtualPathContext( VirtualPathContext context, IDictionary acceptedValues) { // We want to build the set of values that would be provided if this route were to generated // a link and then immediately match it. This includes all the accepted parameter values, and // the defaults. Accepted values that would go in the query string aren't included. var providedValues = new RouteValueDictionary(); foreach (var parameter in _parsedTemplate.Parameters) { object value; if (acceptedValues.TryGetValue(parameter.Name, out value)) { providedValues.Add(parameter.Name, value); } } foreach (var kvp in _defaults) { if (!providedValues.ContainsKey(kvp.Key)) { providedValues.Add(kvp.Key, kvp.Value); } } return new VirtualPathContext(context.Context, context.AmbientValues, context.Values) { ProvidedValues = providedValues, }; } private static IReadOnlyDictionary GetConstraints( IInlineConstraintResolver inlineConstraintResolver, string template, RouteTemplate parsedTemplate, IDictionary constraints) { var constraintBuilder = new RouteConstraintBuilder(inlineConstraintResolver, template); if (constraints != null) { foreach (var kvp in constraints) { constraintBuilder.AddConstraint(kvp.Key, kvp.Value); } } foreach (var parameter in parsedTemplate.Parameters) { foreach (var inlineConstraint in parameter.InlineConstraints) { constraintBuilder.AddResolvedConstraint(parameter.Name, inlineConstraint.Constraint); } } return constraintBuilder.Build(); } private static RouteValueDictionary GetDefaults( RouteTemplate parsedTemplate, IDictionary defaults) { // Do not use RouteValueDictionary.Empty for defaults, it might be modified inside // UpdateInlineDefaultValuesAndConstraints() var result = defaults == null ? new RouteValueDictionary() : new RouteValueDictionary(defaults); foreach (var parameter in parsedTemplate.Parameters) { if (parameter.DefaultValue != null) { if (result.ContainsKey(parameter.Name)) { throw new InvalidOperationException( Resources.FormatTemplateRoute_CannotHaveDefaultValueSpecifiedInlineAndExplicitly( parameter.Name)); } else { result.Add(parameter.Name, parameter.DefaultValue); } } } return result; } private TemplateRouteRouteAsyncValues CreateRouteAsyncValues( string requestPath, IDictionary producedValues, bool matchedValues, bool matchedConstraints, bool handled) { var values = new TemplateRouteRouteAsyncValues(); values.Template = _routeTemplate; values.RequestPath = requestPath; values.DefaultValues = Defaults; values.ProducedValues = producedValues; values.Constraints = _constraints; values.Target = _target; values.MatchedTemplate = matchedValues; values.MatchedConstraints = matchedConstraints; values.Matched = matchedValues && matchedConstraints; values.Handled = handled; return values; } private static void MergeValues( IDictionary destination, IDictionary values) { foreach (var kvp in values) { // This will replace the original value for the specified key. // Values from the matched route will take preference over previous // data in the route context. destination[kvp.Key] = kvp.Value; } } // Needed because IDictionary<> is not an IReadOnlyDictionary<> private static void MergeValues( IDictionary destination, IReadOnlyDictionary values) { foreach (var kvp in values) { // This will replace the original value for the specified key. // Values from the matched route will take preference over previous // data in the route context. destination[kvp.Key] = kvp.Value; } } private void EnsureLoggers(HttpContext context) { if (_logger == null) { var factory = context.RequestServices.GetRequiredService(); _logger = factory.Create(); _constraintLogger = factory.Create(typeof(RouteConstraintMatcher).FullName); } } public override string ToString() { return _routeTemplate; } } }