GTLRURITemplate.m 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508
  1. /* Copyright (c) 2010 Google Inc.
  2. *
  3. * Licensed under the Apache License, Version 2.0 (the "License");
  4. * you may not use this file except in compliance with the License.
  5. * You may obtain a copy of the License at
  6. *
  7. * http://www.apache.org/licenses/LICENSE-2.0
  8. *
  9. * Unless required by applicable law or agreed to in writing, software
  10. * distributed under the License is distributed on an "AS IS" BASIS,
  11. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. * See the License for the specific language governing permissions and
  13. * limitations under the License.
  14. */
  15. //#import <GoogleAPIClientForREST/GTLRURITemplate.h>
  16. #import "GTLRURITemplate.h"
  17. // Key constants for handling variables.
  18. static NSString *const kVariable = @"variable"; // NSString
  19. static NSString *const kExplode = @"explode"; // NSString
  20. static NSString *const kPartial = @"partial"; // NSString
  21. static NSString *const kPartialValue = @"partialValue"; // NSNumber
  22. // Help for passing the Expansion info in one shot.
  23. struct ExpansionInfo {
  24. // Constant for the whole expansion.
  25. unichar expressionOperator;
  26. __unsafe_unretained NSString *joiner;
  27. BOOL allowReservedInEscape;
  28. // Update for each variable.
  29. __unsafe_unretained NSString *explode;
  30. };
  31. // Helper just to shorten the lines when needed.
  32. static NSString *UnescapeString(NSString *str) {
  33. return [str stringByRemovingPercentEncoding];
  34. }
  35. static NSString *EscapeString(NSString *str, BOOL allowReserved) {
  36. // The spec is a little hard to map onto the charsets, so force
  37. // reserved bits in/out.
  38. NSMutableCharacterSet *cs = [[NSCharacterSet URLQueryAllowedCharacterSet] mutableCopy];
  39. NSString * const kReservedChars = @":/?#[]@!$&'()*+,;=";
  40. if (allowReserved) {
  41. [cs addCharactersInString:kReservedChars];
  42. } else {
  43. [cs removeCharactersInString:kReservedChars];
  44. }
  45. NSString *resultStr = [str stringByAddingPercentEncodingWithAllowedCharacters:cs];
  46. return resultStr;
  47. }
  48. static NSString *StringFromNSNumber(NSNumber *rawValue) {
  49. NSString *strValue;
  50. // NSNumber doesn't expose a way to tell if it is holding a BOOL or something
  51. // else. -[NSNumber objCType] for a BOOL is the same as @encoding(char), but
  52. // in the 64bit runtine @encoding(BOOL) (or for "bool") won't match that as
  53. // the 64bit runtime actually has a true boolean type. Instead we reply on
  54. // checking if the numbers are the CFBoolean constants to force true/value
  55. // values.
  56. if ((rawValue == (NSNumber *)kCFBooleanTrue) ||
  57. (rawValue == (NSNumber *)kCFBooleanFalse)) {
  58. strValue = (rawValue.boolValue ? @"true" : @"false");
  59. } else {
  60. strValue = [rawValue stringValue];
  61. }
  62. return strValue;
  63. }
  64. @implementation GTLRURITemplate
  65. #pragma mark Internal Helpers
  66. + (BOOL)parseExpression:(NSString *)expression
  67. expressionOperator:(unichar*)outExpressionOperator
  68. variables:(NSMutableArray **)outVariables
  69. defaultValues:(NSMutableDictionary **)outDefaultValues {
  70. // Please see the spec for full details, but here are the basics:
  71. //
  72. // URI-Template = *( literals / expression )
  73. // expression = "{" [ operator ] variable-list "}"
  74. // variable-list = varspec *( "," varspec )
  75. // varspec = varname [ modifier ] [ "=" default ]
  76. // varname = varchar *( varchar / "." )
  77. // modifier = explode / partial
  78. // explode = ( "*" / "+" )
  79. // partial = ( substring / remainder ) offset
  80. //
  81. // Examples:
  82. // http://www.example.com/foo{?query,number}
  83. // http://maps.com/mapper{?address*}
  84. // http://directions.org/directions{?from+,to+}
  85. // http://search.org/query{?terms+=none}
  86. //
  87. // http://tools.ietf.org/html/draft-gregorio-uritemplate-04#section-2.2
  88. // Operator and op-reserve characters
  89. static NSCharacterSet *operatorSet = nil;
  90. // http://tools.ietf.org/html/draft-gregorio-uritemplate-04#section-2.4.1
  91. // Explode characters
  92. static NSCharacterSet *explodeSet = nil;
  93. // http://tools.ietf.org/html/draft-gregorio-uritemplate-04#section-2.4.2
  94. // Partial (prefix/subset) characters
  95. static NSCharacterSet *partialSet = nil;
  96. static dispatch_once_t onceToken;
  97. dispatch_once(&onceToken, ^{
  98. operatorSet = [NSCharacterSet characterSetWithCharactersInString:@"+./;?|!@"];
  99. explodeSet = [NSCharacterSet characterSetWithCharactersInString:@"*+"];
  100. partialSet = [NSCharacterSet characterSetWithCharactersInString:@":^"];
  101. });
  102. // http://tools.ietf.org/html/draft-gregorio-uritemplate-04#section-3.3
  103. // Empty expression inlines the expression.
  104. if (expression.length == 0) return NO;
  105. // Pull off any operator.
  106. *outExpressionOperator = 0;
  107. unichar firstChar = [expression characterAtIndex:0];
  108. if ([operatorSet characterIsMember:firstChar]) {
  109. *outExpressionOperator = firstChar;
  110. expression = [expression substringFromIndex:1];
  111. }
  112. if (expression.length == 0) return NO;
  113. // Need to find atleast one varspec for the expresssion to be considered
  114. // valid.
  115. BOOL gotAVarspec = NO;
  116. // Split the variable list.
  117. NSArray *varspecs = [expression componentsSeparatedByString:@","];
  118. // Extract the defaults, explodes and modifiers from the varspecs.
  119. *outVariables = [NSMutableArray arrayWithCapacity:varspecs.count];
  120. for (__strong NSString *varspec in varspecs) {
  121. NSString *defaultValue = nil;
  122. if (varspec.length == 0) continue;
  123. NSMutableDictionary *varInfo =
  124. [NSMutableDictionary dictionaryWithCapacity:4];
  125. // Check for a default (foo=bar).
  126. NSRange range = [varspec rangeOfString:@"="];
  127. if (range.location != NSNotFound) {
  128. defaultValue =
  129. UnescapeString([varspec substringFromIndex:range.location + 1]);
  130. varspec = [varspec substringToIndex:range.location];
  131. if (varspec.length == 0) continue;
  132. }
  133. // Check for explode (foo*).
  134. NSUInteger lenLessOne = varspec.length - 1;
  135. if ([explodeSet characterIsMember:[varspec characterAtIndex:lenLessOne]]) {
  136. [varInfo setObject:[varspec substringFromIndex:lenLessOne] forKey:kExplode];
  137. varspec = [varspec substringToIndex:lenLessOne];
  138. if (varspec.length == 0) continue;
  139. } else {
  140. // Check for partial (prefix/suffix) (foo:12).
  141. range = [varspec rangeOfCharacterFromSet:partialSet];
  142. if (range.location != NSNotFound) {
  143. NSString *partialMode = [varspec substringWithRange:range];
  144. NSString *valueStr = [varspec substringFromIndex:range.location + 1];
  145. // If there wasn't a value for the partial, ignore it.
  146. if (valueStr.length > 0) {
  147. [varInfo setObject:partialMode forKey:kPartial];
  148. // TODO: Should validate valueStr is just a number...
  149. [varInfo setObject:[NSNumber numberWithInteger:[valueStr integerValue]]
  150. forKey:kPartialValue];
  151. }
  152. varspec = [varspec substringToIndex:range.location];
  153. if (varspec.length == 0) continue;
  154. }
  155. }
  156. // Spec allows percent escaping in names, so undo that.
  157. varspec = UnescapeString(varspec);
  158. // Save off the cleaned up variable name.
  159. [varInfo setObject:varspec forKey:kVariable];
  160. [*outVariables addObject:varInfo];
  161. gotAVarspec = YES;
  162. // Now that the variable has been cleaned up, store its default.
  163. if (defaultValue) {
  164. if (*outDefaultValues == nil) {
  165. *outDefaultValues = [NSMutableDictionary dictionary];
  166. }
  167. [*outDefaultValues setObject:defaultValue forKey:varspec];
  168. }
  169. }
  170. // All done.
  171. return gotAVarspec;
  172. }
  173. + (NSString *)expandVariables:(NSArray *)variables
  174. expressionOperator:(unichar)expressionOperator
  175. values:(NSDictionary *)valueProvider
  176. defaultValues:(NSMutableDictionary *)defaultValues {
  177. NSString *prefix = nil;
  178. struct ExpansionInfo expansionInfo = {
  179. .expressionOperator = expressionOperator,
  180. .joiner = nil,
  181. .allowReservedInEscape = NO,
  182. .explode = nil,
  183. };
  184. switch (expressionOperator) {
  185. case 0:
  186. expansionInfo.joiner = @",";
  187. prefix = @"";
  188. break;
  189. case '+':
  190. expansionInfo.joiner = @",";
  191. prefix = @"";
  192. // The reserved character are safe from escaping.
  193. expansionInfo.allowReservedInEscape = YES;
  194. break;
  195. case '.':
  196. expansionInfo.joiner = @".";
  197. prefix = @".";
  198. break;
  199. case '/':
  200. expansionInfo.joiner = @"/";
  201. prefix = @"/";
  202. break;
  203. case ';':
  204. expansionInfo.joiner = @";";
  205. prefix = @";";
  206. break;
  207. case '?':
  208. expansionInfo.joiner = @"&";
  209. prefix = @"?";
  210. break;
  211. default:
  212. [NSException raise:@"GTLRURITemplateUnsupported"
  213. format:@"Unknown expression operator '%C'", expressionOperator];
  214. break;
  215. }
  216. NSMutableArray *results = [NSMutableArray arrayWithCapacity:variables.count];
  217. for (NSDictionary *varInfo in variables) {
  218. NSString *variable = [varInfo objectForKey:kVariable];
  219. expansionInfo.explode = [varInfo objectForKey:kExplode];
  220. // Look up the variable value.
  221. id rawValue = [valueProvider objectForKey:variable];
  222. // If the value is an empty array or dictionary, the default is still used.
  223. if (([rawValue isKindOfClass:[NSArray class]]
  224. || [rawValue isKindOfClass:[NSDictionary class]])
  225. && ((NSArray *)rawValue).count == 0) {
  226. rawValue = nil;
  227. }
  228. // Got nothing? Check defaults.
  229. if (rawValue == nil) {
  230. rawValue = [defaultValues objectForKey:variable];
  231. }
  232. // If we didn't get any value, on to the next thing.
  233. if (!rawValue) {
  234. continue;
  235. }
  236. // Time do to the work...
  237. NSString *result = nil;
  238. if ([rawValue isKindOfClass:[NSString class]]) {
  239. result = [self expandString:rawValue
  240. variableName:variable
  241. expansionInfo:&expansionInfo];
  242. } else if ([rawValue isKindOfClass:[NSNumber class]]) {
  243. // Turn the number into a string and send it on its way.
  244. NSString *strValue = StringFromNSNumber(rawValue);
  245. result = [self expandString:strValue
  246. variableName:variable
  247. expansionInfo:&expansionInfo];
  248. } else if ([rawValue isKindOfClass:[NSArray class]]) {
  249. result = [self expandArray:rawValue
  250. variableName:variable
  251. expansionInfo:&expansionInfo];
  252. } else if ([rawValue isKindOfClass:[NSDictionary class]]) {
  253. result = [self expandDictionary:rawValue
  254. variableName:variable
  255. expansionInfo:&expansionInfo];
  256. } else {
  257. [NSException raise:@"GTLRURITemplateUnsupported"
  258. format:@"Variable returned unsupported type (%@)",
  259. NSStringFromClass([rawValue class])];
  260. }
  261. // Did it generate anything?
  262. if (result == nil)
  263. continue;
  264. // Apply partial.
  265. // Defaults should get partial applied?
  266. // ( http://tools.ietf.org/html/draft-gregorio-uritemplate-04#section-2.5 )
  267. NSString *partial = [varInfo objectForKey:kPartial];
  268. if (partial.length > 0) {
  269. [NSException raise:@"GTLRURITemplateUnsupported"
  270. format:@"Unsupported partial on expansion %@", partial];
  271. }
  272. // Add the result
  273. [results addObject:result];
  274. }
  275. // Join and add any needed prefix.
  276. NSString *joinedResults =
  277. [results componentsJoinedByString:expansionInfo.joiner];
  278. if ((prefix.length > 0) && (joinedResults.length > 0)) {
  279. return [prefix stringByAppendingString:joinedResults];
  280. }
  281. return joinedResults;
  282. }
  283. + (NSString *)expandString:(NSString *)valueStr
  284. variableName:(NSString *)variableName
  285. expansionInfo:(struct ExpansionInfo *)expansionInfo {
  286. NSString *escapedValue =
  287. EscapeString(valueStr, expansionInfo->allowReservedInEscape);
  288. switch (expansionInfo->expressionOperator) {
  289. case ';':
  290. case '?':
  291. if (valueStr.length > 0) {
  292. return [NSString stringWithFormat:@"%@=%@", variableName, escapedValue];
  293. }
  294. return variableName;
  295. default:
  296. return escapedValue;
  297. }
  298. }
  299. + (NSString *)expandArray:(NSArray *)valueArray
  300. variableName:(NSString *)variableName
  301. expansionInfo:(struct ExpansionInfo *)expansionInfo {
  302. NSMutableArray *results = [NSMutableArray arrayWithCapacity:valueArray.count];
  303. // When joining variable with value, use "var.val" except for 'path' and
  304. // 'form' style expression, use 'var=val' then.
  305. char variableValueJoiner = '.';
  306. unichar expressionOperator = expansionInfo->expressionOperator;
  307. if ((expressionOperator == ';') || (expressionOperator == '?')) {
  308. variableValueJoiner = '=';
  309. }
  310. // Loop over the values.
  311. for (id rawValue in valueArray) {
  312. NSString *value;
  313. if ([rawValue isKindOfClass:[NSNumber class]]) {
  314. value = StringFromNSNumber((id)rawValue);
  315. } else if ([rawValue isKindOfClass:[NSString class]]) {
  316. value = rawValue;
  317. } else {
  318. [NSException raise:@"GTLRURITemplateUnsupported"
  319. format:@"Variable '%@' returned NSArray with unsupported type (%@), array: %@",
  320. variableName, NSStringFromClass([rawValue class]), valueArray];
  321. }
  322. // Escape it.
  323. value = EscapeString(value, expansionInfo->allowReservedInEscape);
  324. // Should variable names be used?
  325. if ([expansionInfo->explode isEqual:@"+"]) {
  326. value = [NSString stringWithFormat:@"%@%c%@",
  327. variableName, variableValueJoiner, value];
  328. }
  329. [results addObject:value];
  330. }
  331. if (results.count > 0) {
  332. // Use the default joiner unless there was no explode request, then a list
  333. // always gets comma seperated.
  334. NSString *joiner = expansionInfo->joiner;
  335. if (expansionInfo->explode == nil) {
  336. joiner = @",";
  337. }
  338. // Join the values.
  339. NSString *joined = [results componentsJoinedByString:joiner];
  340. // 'form' style without an explode gets the variable name set to the
  341. // joined list of values.
  342. if ((expressionOperator == '?') && (expansionInfo->explode == nil)) {
  343. return [NSString stringWithFormat:@"%@=%@", variableName, joined];
  344. }
  345. return joined;
  346. }
  347. return nil;
  348. }
  349. + (NSString *)expandDictionary:(NSDictionary *)valueDict
  350. variableName:(NSString *)variableName
  351. expansionInfo:(struct ExpansionInfo *)expansionInfo {
  352. NSMutableArray *results = [NSMutableArray arrayWithCapacity:valueDict.count];
  353. // When joining variable with value:
  354. // - Default to the joiner...
  355. // - No explode, always comma...
  356. // - For 'path' and 'form' style expression, use 'var=val'.
  357. NSString *keyValueJoiner = expansionInfo->joiner;
  358. unichar expressionOperator = expansionInfo->expressionOperator;
  359. if (expansionInfo->explode == nil) {
  360. keyValueJoiner = @",";
  361. } else if ((expressionOperator == ';') || (expressionOperator == '?')) {
  362. keyValueJoiner = @"=";
  363. }
  364. // Loop over the sorted keys.
  365. NSArray *sortedKeys = [valueDict.allKeys sortedArrayUsingSelector:@selector(compare:)];
  366. for (__strong NSString *key in sortedKeys) {
  367. NSString *value = [valueDict objectForKey:key];
  368. // Escape them.
  369. key = EscapeString(key, expansionInfo->allowReservedInEscape);
  370. value = EscapeString(value, expansionInfo->allowReservedInEscape);
  371. // Should variable names be used?
  372. if ([expansionInfo->explode isEqual:@"+"]) {
  373. key = [NSString stringWithFormat:@"%@.%@", variableName, key];
  374. }
  375. if ((expressionOperator == '?' || expressionOperator == ';')
  376. && (value.length == 0)) {
  377. [results addObject:key];
  378. } else {
  379. NSString *pair = [NSString stringWithFormat:@"%@%@%@",
  380. key, keyValueJoiner, value];
  381. [results addObject:pair];
  382. }
  383. }
  384. if (results.count) {
  385. // Use the default joiner unless there was no explode request, then a list
  386. // always gets comma seperated.
  387. NSString *joiner = expansionInfo->joiner;
  388. if (expansionInfo->explode == nil) {
  389. joiner = @",";
  390. }
  391. // Join the values.
  392. NSString *joined = [results componentsJoinedByString:joiner];
  393. // 'form' style without an explode gets the variable name set to the
  394. // joined list of values.
  395. if ((expressionOperator == '?') && (expansionInfo->explode == nil)) {
  396. return [NSString stringWithFormat:@"%@=%@", variableName, joined];
  397. }
  398. return joined;
  399. }
  400. return nil;
  401. }
  402. #pragma mark Public API
  403. + (NSString *)expandTemplate:(NSString *)uriTemplate
  404. values:(NSDictionary *)valueProvider {
  405. NSMutableString *result =
  406. [NSMutableString stringWithCapacity:uriTemplate.length];
  407. NSScanner *scanner = [NSScanner scannerWithString:uriTemplate];
  408. [scanner setCharactersToBeSkipped:nil];
  409. // Defaults have to live through the full evaluation, so if any are encoured
  410. // they are reused throughout the expansion calls.
  411. NSMutableDictionary *defaultValues = nil;
  412. // Pull out the expressions for processing.
  413. while (![scanner isAtEnd]) {
  414. NSString *skipped = nil;
  415. // Find the next '{'.
  416. if ([scanner scanUpToString:@"{" intoString:&skipped]) {
  417. // Add anything before it to the result.
  418. [result appendString:skipped];
  419. }
  420. // Advance over the '{'.
  421. [scanner scanString:@"{" intoString:nil];
  422. // Collect the expression.
  423. NSString *expression = nil;
  424. if ([scanner scanUpToString:@"}" intoString:&expression]) {
  425. // Collect the trailing '}' on the expression.
  426. BOOL hasTrailingBrace = [scanner scanString:@"}" intoString:nil];
  427. // Parse the expression.
  428. NSMutableArray *variables = nil;
  429. unichar expressionOperator = 0;
  430. if ([self parseExpression:expression
  431. expressionOperator:&expressionOperator
  432. variables:&variables
  433. defaultValues:&defaultValues]) {
  434. // Do the expansion.
  435. NSString *substitution = [self expandVariables:variables
  436. expressionOperator:expressionOperator
  437. values:valueProvider
  438. defaultValues:defaultValues];
  439. if (substitution) {
  440. [result appendString:substitution];
  441. }
  442. } else {
  443. // Failed to parse, add the raw expression to the output.
  444. if (hasTrailingBrace) {
  445. [result appendFormat:@"{%@}", expression];
  446. } else {
  447. [result appendFormat:@"{%@", expression];
  448. }
  449. }
  450. } else if (![scanner isAtEnd]) {
  451. // Empty expression ('{}'). Copy over the opening brace and the trailing
  452. // one will be copied by the next cycle of the loop.
  453. [result appendString:@"{"];
  454. }
  455. }
  456. return result;
  457. }
  458. @end