/core/externals/google-toolbox-for-mac/Foundation/GTMURITemplate.m

http://macfuse.googlecode.com/ · Objective C · 523 lines · 355 code · 57 blank · 111 comment · 98 complexity · 4a0aa3209796a19be5e6726d3f619520 MD5 · raw file

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