PageRenderTime 114ms CodeModel.GetById 14ms app.highlight 94ms RepoModel.GetById 2ms app.codeStats 0ms

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