RestPath.cs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Text;
  5. using MediaBrowser.Model.Logging;
  6. using ServiceStack.Serialization;
  7. namespace ServiceStack
  8. {
  9. public class RestPath
  10. {
  11. private const string WildCard = "*";
  12. private const char WildCardChar = '*';
  13. private const string PathSeperator = "/";
  14. private const char PathSeperatorChar = '/';
  15. private const char ComponentSeperator = '.';
  16. private const string VariablePrefix = "{";
  17. readonly bool[] componentsWithSeparators;
  18. private readonly string restPath;
  19. private readonly string allowedVerbs;
  20. private readonly bool allowsAllVerbs;
  21. public bool IsWildCardPath { get; private set; }
  22. private readonly string[] literalsToMatch;
  23. private readonly string[] variablesNames;
  24. private readonly bool[] isWildcard;
  25. private readonly int wildcardCount = 0;
  26. public int VariableArgsCount { get; set; }
  27. /// <summary>
  28. /// The number of segments separated by '/' determinable by path.Split('/').Length
  29. /// e.g. /path/to/here.ext == 3
  30. /// </summary>
  31. public int PathComponentsCount { get; set; }
  32. /// <summary>
  33. /// The total number of segments after subparts have been exploded ('.')
  34. /// e.g. /path/to/here.ext == 4
  35. /// </summary>
  36. public int TotalComponentsCount { get; set; }
  37. public string[] Verbs
  38. {
  39. get
  40. {
  41. return allowsAllVerbs
  42. ? new[] { "ANY" }
  43. : AllowedVerbs.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries);
  44. }
  45. }
  46. public Type RequestType { get; private set; }
  47. public string Path { get { return this.restPath; } }
  48. public string Summary { get; private set; }
  49. public string Notes { get; private set; }
  50. public bool AllowsAllVerbs { get { return this.allowsAllVerbs; } }
  51. public string AllowedVerbs { get { return this.allowedVerbs; } }
  52. public int Priority { get; set; } //passed back to RouteAttribute
  53. public static string[] GetPathPartsForMatching(string pathInfo)
  54. {
  55. var parts = pathInfo.ToLower().Split(PathSeperatorChar)
  56. .Where(x => !string.IsNullOrEmpty(x)).ToArray();
  57. return parts;
  58. }
  59. public static List<string> GetFirstMatchHashKeys(string[] pathPartsForMatching)
  60. {
  61. var hashPrefix = pathPartsForMatching.Length + PathSeperator;
  62. return GetPotentialMatchesWithPrefix(hashPrefix, pathPartsForMatching);
  63. }
  64. public static List<string> GetFirstMatchWildCardHashKeys(string[] pathPartsForMatching)
  65. {
  66. const string hashPrefix = WildCard + PathSeperator;
  67. return GetPotentialMatchesWithPrefix(hashPrefix, pathPartsForMatching);
  68. }
  69. private static List<string> GetPotentialMatchesWithPrefix(string hashPrefix, string[] pathPartsForMatching)
  70. {
  71. var list = new List<string>();
  72. foreach (var part in pathPartsForMatching)
  73. {
  74. list.Add(hashPrefix + part);
  75. var subParts = part.Split(ComponentSeperator);
  76. if (subParts.Length == 1) continue;
  77. foreach (var subPart in subParts)
  78. {
  79. list.Add(hashPrefix + subPart);
  80. }
  81. }
  82. return list;
  83. }
  84. public RestPath(Type requestType, string path, string verbs, string summary = null, string notes = null)
  85. {
  86. this.RequestType = requestType;
  87. this.Summary = summary;
  88. this.Notes = notes;
  89. this.restPath = path;
  90. this.allowsAllVerbs = verbs == null || string.Equals(verbs, WildCard, StringComparison.OrdinalIgnoreCase);
  91. if (!this.allowsAllVerbs)
  92. {
  93. this.allowedVerbs = verbs.ToUpper();
  94. }
  95. var componentsList = new List<string>();
  96. //We only split on '.' if the restPath has them. Allows for /{action}.{type}
  97. var hasSeparators = new List<bool>();
  98. foreach (var component in this.restPath.Split(PathSeperatorChar))
  99. {
  100. if (string.IsNullOrEmpty(component)) continue;
  101. if (StringContains(component, VariablePrefix)
  102. && component.IndexOf(ComponentSeperator) != -1)
  103. {
  104. hasSeparators.Add(true);
  105. componentsList.AddRange(component.Split(ComponentSeperator));
  106. }
  107. else
  108. {
  109. hasSeparators.Add(false);
  110. componentsList.Add(component);
  111. }
  112. }
  113. var components = componentsList.ToArray();
  114. this.TotalComponentsCount = components.Length;
  115. this.literalsToMatch = new string[this.TotalComponentsCount];
  116. this.variablesNames = new string[this.TotalComponentsCount];
  117. this.isWildcard = new bool[this.TotalComponentsCount];
  118. this.componentsWithSeparators = hasSeparators.ToArray();
  119. this.PathComponentsCount = this.componentsWithSeparators.Length;
  120. string firstLiteralMatch = null;
  121. var sbHashKey = new StringBuilder();
  122. for (var i = 0; i < components.Length; i++)
  123. {
  124. var component = components[i];
  125. if (component.StartsWith(VariablePrefix))
  126. {
  127. var variableName = component.Substring(1, component.Length - 2);
  128. if (variableName[variableName.Length - 1] == WildCardChar)
  129. {
  130. this.isWildcard[i] = true;
  131. variableName = variableName.Substring(0, variableName.Length - 1);
  132. }
  133. this.variablesNames[i] = variableName;
  134. this.VariableArgsCount++;
  135. }
  136. else
  137. {
  138. this.literalsToMatch[i] = component.ToLower();
  139. sbHashKey.Append(i + PathSeperatorChar.ToString() + this.literalsToMatch);
  140. if (firstLiteralMatch == null)
  141. {
  142. firstLiteralMatch = this.literalsToMatch[i];
  143. }
  144. }
  145. }
  146. for (var i = 0; i < components.Length - 1; i++)
  147. {
  148. if (!this.isWildcard[i]) continue;
  149. if (this.literalsToMatch[i + 1] == null)
  150. {
  151. throw new ArgumentException(
  152. "A wildcard path component must be at the end of the path or followed by a literal path component.");
  153. }
  154. }
  155. this.wildcardCount = this.isWildcard.Count(x => x);
  156. this.IsWildCardPath = this.wildcardCount > 0;
  157. this.FirstMatchHashKey = !this.IsWildCardPath
  158. ? this.PathComponentsCount + PathSeperator + firstLiteralMatch
  159. : WildCardChar + PathSeperator + firstLiteralMatch;
  160. this.IsValid = sbHashKey.Length > 0;
  161. this.UniqueMatchHashKey = sbHashKey.ToString();
  162. this.typeDeserializer = new StringMapTypeDeserializer(this.RequestType);
  163. RegisterCaseInsenstivePropertyNameMappings();
  164. }
  165. private void RegisterCaseInsenstivePropertyNameMappings()
  166. {
  167. foreach (var propertyInfo in RequestType.GetSerializableProperties())
  168. {
  169. var propertyName = propertyInfo.Name;
  170. propertyNamesMap.Add(propertyName.ToLower(), propertyName);
  171. }
  172. }
  173. public bool IsValid { get; set; }
  174. /// <summary>
  175. /// Provide for quick lookups based on hashes that can be determined from a request url
  176. /// </summary>
  177. public string FirstMatchHashKey { get; private set; }
  178. public string UniqueMatchHashKey { get; private set; }
  179. private readonly StringMapTypeDeserializer typeDeserializer;
  180. private readonly Dictionary<string, string> propertyNamesMap = new Dictionary<string, string>();
  181. public int MatchScore(string httpMethod, string[] withPathInfoParts, ILogger logger)
  182. {
  183. int wildcardMatchCount;
  184. var isMatch = IsMatch(httpMethod, withPathInfoParts, logger, out wildcardMatchCount);
  185. if (!isMatch)
  186. {
  187. return -1;
  188. }
  189. var score = 0;
  190. //Routes with least wildcard matches get the highest score
  191. score += Math.Max((100 - wildcardMatchCount), 1) * 1000;
  192. //Routes with less variable (and more literal) matches
  193. score += Math.Max((10 - VariableArgsCount), 1) * 100;
  194. //Exact verb match is better than ANY
  195. var exactVerb = string.Equals(httpMethod, AllowedVerbs, StringComparison.OrdinalIgnoreCase);
  196. score += exactVerb ? 10 : 1;
  197. return score;
  198. }
  199. private bool StringContains(string str1, string str2)
  200. {
  201. return str1.IndexOf(str2, StringComparison.OrdinalIgnoreCase) != -1;
  202. }
  203. /// <summary>
  204. /// For performance withPathInfoParts should already be a lower case string
  205. /// to minimize redundant matching operations.
  206. /// </summary>
  207. public bool IsMatch(string httpMethod, string[] withPathInfoParts, ILogger logger, out int wildcardMatchCount)
  208. {
  209. wildcardMatchCount = 0;
  210. if (withPathInfoParts.Length != this.PathComponentsCount && !this.IsWildCardPath)
  211. {
  212. //logger.Info("withPathInfoParts mismatch for {0} for {1}", httpMethod, string.Join("/", withPathInfoParts));
  213. return false;
  214. }
  215. if (!this.allowsAllVerbs && !StringContains(this.allowedVerbs, httpMethod))
  216. {
  217. //logger.Info("allowsAllVerbs mismatch for {0} for {1} allowedverbs {2}", httpMethod, string.Join("/", withPathInfoParts), this.allowedVerbs);
  218. return false;
  219. }
  220. if (!ExplodeComponents(ref withPathInfoParts))
  221. {
  222. //logger.Info("ExplodeComponents mismatch for {0} for {1}", httpMethod, string.Join("/", withPathInfoParts));
  223. return false;
  224. }
  225. if (this.TotalComponentsCount != withPathInfoParts.Length && !this.IsWildCardPath)
  226. {
  227. //logger.Info("TotalComponentsCount mismatch for {0} for {1}", httpMethod, string.Join("/", withPathInfoParts));
  228. return false;
  229. }
  230. int pathIx = 0;
  231. for (var i = 0; i < this.TotalComponentsCount; i++)
  232. {
  233. if (this.isWildcard[i])
  234. {
  235. if (i < this.TotalComponentsCount - 1)
  236. {
  237. // Continue to consume up until a match with the next literal
  238. while (pathIx < withPathInfoParts.Length && !LiteralsEqual(withPathInfoParts[pathIx], this.literalsToMatch[i + 1]))
  239. {
  240. pathIx++;
  241. wildcardMatchCount++;
  242. }
  243. // Ensure there are still enough parts left to match the remainder
  244. if ((withPathInfoParts.Length - pathIx) < (this.TotalComponentsCount - i - 1))
  245. {
  246. //logger.Info("withPathInfoParts length mismatch for {0} for {1}", httpMethod, string.Join("/", withPathInfoParts));
  247. return false;
  248. }
  249. }
  250. else
  251. {
  252. // A wildcard at the end matches the remainder of path
  253. wildcardMatchCount += withPathInfoParts.Length - pathIx;
  254. pathIx = withPathInfoParts.Length;
  255. }
  256. }
  257. else
  258. {
  259. var literalToMatch = this.literalsToMatch[i];
  260. if (literalToMatch == null)
  261. {
  262. // Matching an ordinary (non-wildcard) variable consumes a single part
  263. pathIx++;
  264. continue;
  265. }
  266. if (withPathInfoParts.Length <= pathIx || !LiteralsEqual(withPathInfoParts[pathIx], literalToMatch))
  267. {
  268. //logger.Info("withPathInfoParts2 length mismatch for {0} for {1}. not equals: {2} != {3}.", httpMethod, string.Join("/", withPathInfoParts), withPathInfoParts[pathIx], literalToMatch);
  269. return false;
  270. }
  271. pathIx++;
  272. }
  273. }
  274. return pathIx == withPathInfoParts.Length;
  275. }
  276. private bool LiteralsEqual(string str1, string str2)
  277. {
  278. // Most cases
  279. if (string.Equals(str1, str2, StringComparison.OrdinalIgnoreCase))
  280. {
  281. return true;
  282. }
  283. // Handle turkish i
  284. str1 = str1.ToUpperInvariant();
  285. str2 = str2.ToUpperInvariant();
  286. // Invariant IgnoreCase would probably be better but it's not available in PCL
  287. return string.Equals(str1, str2, StringComparison.CurrentCultureIgnoreCase);
  288. }
  289. private bool ExplodeComponents(ref string[] withPathInfoParts)
  290. {
  291. var totalComponents = new List<string>();
  292. for (var i = 0; i < withPathInfoParts.Length; i++)
  293. {
  294. var component = withPathInfoParts[i];
  295. if (string.IsNullOrEmpty(component)) continue;
  296. if (this.PathComponentsCount != this.TotalComponentsCount
  297. && this.componentsWithSeparators[i])
  298. {
  299. var subComponents = component.Split(ComponentSeperator);
  300. if (subComponents.Length < 2) return false;
  301. totalComponents.AddRange(subComponents);
  302. }
  303. else
  304. {
  305. totalComponents.Add(component);
  306. }
  307. }
  308. withPathInfoParts = totalComponents.ToArray();
  309. return true;
  310. }
  311. public object CreateRequest(string pathInfo, Dictionary<string, string> queryStringAndFormData, object fromInstance)
  312. {
  313. var requestComponents = pathInfo.Split(PathSeperatorChar)
  314. .Where(x => !string.IsNullOrEmpty(x)).ToArray();
  315. ExplodeComponents(ref requestComponents);
  316. if (requestComponents.Length != this.TotalComponentsCount)
  317. {
  318. var isValidWildCardPath = this.IsWildCardPath
  319. && requestComponents.Length >= this.TotalComponentsCount - this.wildcardCount;
  320. if (!isValidWildCardPath)
  321. throw new ArgumentException(string.Format(
  322. "Path Mismatch: Request Path '{0}' has invalid number of components compared to: '{1}'",
  323. pathInfo, this.restPath));
  324. }
  325. var requestKeyValuesMap = new Dictionary<string, string>();
  326. var pathIx = 0;
  327. for (var i = 0; i < this.TotalComponentsCount; i++)
  328. {
  329. var variableName = this.variablesNames[i];
  330. if (variableName == null)
  331. {
  332. pathIx++;
  333. continue;
  334. }
  335. string propertyNameOnRequest;
  336. if (!this.propertyNamesMap.TryGetValue(variableName.ToLower(), out propertyNameOnRequest))
  337. {
  338. if (string.Equals("ignore", variableName, StringComparison.OrdinalIgnoreCase))
  339. {
  340. pathIx++;
  341. continue;
  342. }
  343. throw new ArgumentException("Could not find property "
  344. + variableName + " on " + RequestType.GetOperationName());
  345. }
  346. var value = requestComponents.Length > pathIx ? requestComponents[pathIx] : null; //wildcard has arg mismatch
  347. if (value != null && this.isWildcard[i])
  348. {
  349. if (i == this.TotalComponentsCount - 1)
  350. {
  351. // Wildcard at end of path definition consumes all the rest
  352. var sb = new StringBuilder();
  353. sb.Append(value);
  354. for (var j = pathIx + 1; j < requestComponents.Length; j++)
  355. {
  356. sb.Append(PathSeperatorChar + requestComponents[j]);
  357. }
  358. value = sb.ToString();
  359. }
  360. else
  361. {
  362. // Wildcard in middle of path definition consumes up until it
  363. // hits a match for the next element in the definition (which must be a literal)
  364. // It may consume 0 or more path parts
  365. var stopLiteral = i == this.TotalComponentsCount - 1 ? null : this.literalsToMatch[i + 1];
  366. if (!string.Equals(requestComponents[pathIx], stopLiteral, StringComparison.OrdinalIgnoreCase))
  367. {
  368. var sb = new StringBuilder();
  369. sb.Append(value);
  370. pathIx++;
  371. while (!string.Equals(requestComponents[pathIx], stopLiteral, StringComparison.OrdinalIgnoreCase))
  372. {
  373. sb.Append(PathSeperatorChar + requestComponents[pathIx++]);
  374. }
  375. value = sb.ToString();
  376. }
  377. else
  378. {
  379. value = null;
  380. }
  381. }
  382. }
  383. else
  384. {
  385. // Variable consumes single path item
  386. pathIx++;
  387. }
  388. requestKeyValuesMap[propertyNameOnRequest] = value;
  389. }
  390. if (queryStringAndFormData != null)
  391. {
  392. //Query String and form data can override variable path matches
  393. //path variables < query string < form data
  394. foreach (var name in queryStringAndFormData)
  395. {
  396. requestKeyValuesMap[name.Key] = name.Value;
  397. }
  398. }
  399. return this.typeDeserializer.PopulateFromMap(fromInstance, requestKeyValuesMap);
  400. }
  401. public override int GetHashCode()
  402. {
  403. return UniqueMatchHashKey.GetHashCode();
  404. }
  405. }
  406. }