This repository has been archived by the owner on Nov 27, 2018. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 126
/
TemplateBinder.cs
459 lines (409 loc) · 17.5 KB
/
TemplateBinder.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
// Copyright (c) .NET Foundation. 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;
using System.Diagnostics;
using System.Globalization;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Routing.Internal;
using Microsoft.Extensions.ObjectPool;
namespace Microsoft.AspNetCore.Routing.Template
{
public class TemplateBinder
{
private readonly UrlEncoder _urlEncoder;
private readonly ObjectPool<UriBuildingContext> _pool;
private readonly RouteValueDictionary _defaults;
private readonly RouteValueDictionary _filters;
private readonly RouteTemplate _template;
/// <summary>
/// Creates a new instance of <see cref="TemplateBinder"/>.
/// </summary>
/// <param name="urlEncoder">The <see cref="UrlEncoder"/>.</param>
/// <param name="pool">The <see cref="ObjectPool{T}"/>.</param>
/// <param name="template">The <see cref="RouteTemplate"/> to bind values to.</param>
/// <param name="defaults">The default values for <paramref name="template"/>.</param>
public TemplateBinder(
UrlEncoder urlEncoder,
ObjectPool<UriBuildingContext> pool,
RouteTemplate template,
RouteValueDictionary defaults)
{
if (urlEncoder == null)
{
throw new ArgumentNullException(nameof(urlEncoder));
}
if (pool == null)
{
throw new ArgumentNullException(nameof(pool));
}
if (template == null)
{
throw new ArgumentNullException(nameof(template));
}
_urlEncoder = urlEncoder;
_pool = pool;
_template = template;
_defaults = defaults;
// Any default that doesn't have a corresponding parameter is a 'filter' and if a value
// is provided for that 'filter' it must match the value in defaults.
_filters = new RouteValueDictionary(_defaults);
foreach (var parameter in _template.Parameters)
{
_filters.Remove(parameter.Name);
}
}
// Step 1: Get the list of values we're going to try to use to match and generate this URI
public TemplateValuesResult GetValues(RouteValueDictionary ambientValues, RouteValueDictionary values)
{
var context = new TemplateBindingContext(_defaults);
// Find out which entries in the URI are valid for the URI we want to generate.
// If the URI had ordered parameters a="1", b="2", c="3" and the new values
// specified that b="9", then we need to invalidate everything after it. The new
// values should then be a="1", b="9", c=<no value>.
//
// We also handle the case where a parameter is optional but has no value - we shouldn't
// accept additional parameters that appear *after* that parameter.
for (var i = 0; i < _template.Parameters.Count; i++)
{
var parameter = _template.Parameters[i];
// If it's a parameter subsegment, examine the current value to see if it matches the new value
var parameterName = parameter.Name;
object newParameterValue;
var hasNewParameterValue = values.TryGetValue(parameterName, out newParameterValue);
object currentParameterValue = null;
var hasCurrentParameterValue = ambientValues != null &&
ambientValues.TryGetValue(parameterName, out currentParameterValue);
if (hasNewParameterValue && hasCurrentParameterValue)
{
if (!RoutePartsEqual(currentParameterValue, newParameterValue))
{
// Stop copying current values when we find one that doesn't match
break;
}
}
if (!hasNewParameterValue &&
!hasCurrentParameterValue &&
_defaults?.ContainsKey(parameter.Name) != true)
{
// This is an unsatisfied parameter value and there are no defaults. We might still
// be able to generate a URL but we should stop 'accepting' ambient values.
//
// This might be a case like:
// template: a/{b?}/{c?}
// ambient: { c = 17 }
// values: { }
//
// We can still generate a URL from this ("/a") but we shouldn't accept 'c' because
// we can't use it.
//
// In the example above we should fall into this block for 'b'.
break;
}
// If the parameter is a match, add it to the list of values we will use for URI generation
if (hasNewParameterValue)
{
if (IsRoutePartNonEmpty(newParameterValue))
{
context.Accept(parameterName, newParameterValue);
}
}
else
{
if (hasCurrentParameterValue)
{
context.Accept(parameterName, currentParameterValue);
}
}
}
// Add all remaining new values to the list of values we will use for URI generation
foreach (var kvp in values)
{
if (IsRoutePartNonEmpty(kvp.Value))
{
context.Accept(kvp.Key, kvp.Value);
}
}
// Accept all remaining default values if they match a required parameter
for (var i = 0; i < _template.Parameters.Count; i++)
{
var parameter = _template.Parameters[i];
if (parameter.IsOptional || parameter.IsCatchAll)
{
continue;
}
if (context.NeedsValue(parameter.Name))
{
// Add the default value only if there isn't already a new value for it and
// only if it actually has a default value, which we determine based on whether
// the parameter value is required.
context.AcceptDefault(parameter.Name);
}
}
// Validate that all required parameters have a value.
for (var i = 0; i < _template.Parameters.Count; i++)
{
var parameter = _template.Parameters[i];
if (parameter.IsOptional || parameter.IsCatchAll)
{
continue;
}
if (!context.AcceptedValues.ContainsKey(parameter.Name))
{
// We don't have a value for this parameter, so we can't generate a url.
return null;
}
}
// Any default values that don't appear as parameters are treated like filters. Any new values
// provided must match these defaults.
foreach (var filter in _filters)
{
var parameter = GetParameter(filter.Key);
if (parameter != null)
{
continue;
}
object value;
if (values.TryGetValue(filter.Key, out value))
{
if (!RoutePartsEqual(value, filter.Value))
{
// If there is a non-parameterized value in the route and there is a
// new value for it and it doesn't match, this route won't match.
return null;
}
}
}
// Add any ambient values that don't match parameters - they need to be visible to constraints
// but they will ignored by link generation.
var combinedValues = new RouteValueDictionary(context.AcceptedValues);
if (ambientValues != null)
{
foreach (var kvp in ambientValues)
{
if (IsRoutePartNonEmpty(kvp.Value))
{
var parameter = GetParameter(kvp.Key);
if (parameter == null && !context.AcceptedValues.ContainsKey(kvp.Key))
{
combinedValues.Add(kvp.Key, kvp.Value);
}
}
}
}
return new TemplateValuesResult()
{
AcceptedValues = context.AcceptedValues,
CombinedValues = combinedValues,
};
}
// Step 2: If the route is a match generate the appropriate URI
public string BindValues(RouteValueDictionary acceptedValues)
{
var context = _pool.Get();
var result = BindValues(context, acceptedValues);
_pool.Return(context);
return result;
}
private string BindValues(UriBuildingContext context, RouteValueDictionary acceptedValues)
{
for (var i = 0; i < _template.Segments.Count; i++)
{
Debug.Assert(context.BufferState == SegmentState.Beginning);
Debug.Assert(context.UriState == SegmentState.Beginning);
var segment = _template.Segments[i];
for (var j = 0; j < segment.Parts.Count; j++)
{
var part = segment.Parts[j];
if (part.IsLiteral)
{
if (!context.Accept(part.Text))
{
return null;
}
}
else if (part.IsParameter)
{
// If it's a parameter, get its value
object value;
var hasValue = acceptedValues.TryGetValue(part.Name, out value);
if (hasValue)
{
acceptedValues.Remove(part.Name);
}
var isSameAsDefault = false;
object defaultValue;
if (_defaults != null && _defaults.TryGetValue(part.Name, out defaultValue))
{
if (RoutePartsEqual(value, defaultValue))
{
isSameAsDefault = true;
}
}
var converted = Convert.ToString(value, CultureInfo.InvariantCulture);
if (isSameAsDefault)
{
// If the accepted value is the same as the default value buffer it since
// we won't necessarily add it to the URI we generate.
if (!context.Buffer(converted))
{
return null;
}
}
else
{
// If the value is not accepted, it is null or empty value in the
// middle of the segment. We accept this if the parameter is an
// optional parameter and it is preceded by an optional seperator.
// I this case, we need to remove the optional seperator that we
// have added to the URI
// Example: template = {id}.{format?}. parameters: id=5
// In this case after we have generated "5.", we wont find any value
// for format, so we remove '.' and generate 5.
if (!context.Accept(converted))
{
if (j != 0 && part.IsOptional && segment.Parts[j - 1].IsOptionalSeperator)
{
context.Remove(segment.Parts[j - 1].Text);
}
else
{
return null;
}
}
}
}
}
context.EndSegment();
}
// Generate the query string from the remaining values
var wroteFirst = false;
foreach (var kvp in acceptedValues)
{
if (_defaults != null && _defaults.ContainsKey(kvp.Key))
{
// This value is a 'filter' we don't need to put it in the query string.
continue;
}
var values = kvp.Value as IEnumerable;
if (values != null && !(values is string))
{
foreach (var value in values)
{
wroteFirst |= AddParameterToContext(context, kvp.Key, value, wroteFirst);
}
}
else
{
wroteFirst |= AddParameterToContext(context, kvp.Key, kvp.Value, wroteFirst);
}
}
return context.ToString();
}
private bool AddParameterToContext(UriBuildingContext context, string key, object value, bool wroteFirst)
{
var converted = Convert.ToString(value, CultureInfo.InvariantCulture);
if (!string.IsNullOrEmpty(converted))
{
context.Writer.Write(wroteFirst ? '&' : '?');
_urlEncoder.Encode(context.Writer, key);
context.Writer.Write('=');
_urlEncoder.Encode(context.Writer, converted);
return true;
}
return false;
}
private TemplatePart GetParameter(string name)
{
for (var i = 0; i < _template.Parameters.Count; i++)
{
var parameter = _template.Parameters[i];
if (string.Equals(parameter.Name, name, StringComparison.OrdinalIgnoreCase))
{
return parameter;
}
}
return null;
}
/// <summary>
/// Compares two objects for equality as parts of a case-insensitive path.
/// </summary>
/// <param name="a">An object to compare.</param>
/// <param name="b">An object to compare.</param>
/// <returns>True if the object are equal, otherwise false.</returns>
public static bool RoutePartsEqual(object a, object b)
{
var sa = a as string;
var sb = b as string;
if (sa != null && sb != null)
{
// For strings do a case-insensitive comparison
return string.Equals(sa, sb, StringComparison.OrdinalIgnoreCase);
}
else
{
if (a != null && b != null)
{
// Explicitly call .Equals() in case it is overridden in the type
return a.Equals(b);
}
else
{
// At least one of them is null. Return true if they both are
return a == b;
}
}
}
private static bool IsRoutePartNonEmpty(object routePart)
{
var routePartString = routePart as string;
if (routePartString == null)
{
return routePart != null;
}
else
{
return routePartString.Length > 0;
}
}
[DebuggerDisplay("{DebuggerToString(),nq}")]
private struct TemplateBindingContext
{
private readonly RouteValueDictionary _defaults;
private readonly RouteValueDictionary _acceptedValues;
public TemplateBindingContext(RouteValueDictionary defaults)
{
_defaults = defaults;
_acceptedValues = new RouteValueDictionary();
}
public RouteValueDictionary AcceptedValues
{
get { return _acceptedValues; }
}
public void Accept(string key, object value)
{
if (!_acceptedValues.ContainsKey(key))
{
_acceptedValues.Add(key, value);
}
}
public void AcceptDefault(string key)
{
Debug.Assert(!_acceptedValues.ContainsKey(key));
object value;
if (_defaults != null && _defaults.TryGetValue(key, out value))
{
_acceptedValues.Add(key, value);
}
}
public bool NeedsValue(string key)
{
return !_acceptedValues.ContainsKey(key);
}
private string DebuggerToString()
{
return string.Format("{{Accepted: '{0}'}}", string.Join(", ", _acceptedValues.Keys));
}
}
}
}