123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508 |
- /* Copyright (c) 2010 Google Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- //#import <GoogleAPIClientForREST/GTLRURITemplate.h>
- #import "GTLRURITemplate.h"
- // Key constants for handling variables.
- static NSString *const kVariable = @"variable"; // NSString
- static NSString *const kExplode = @"explode"; // NSString
- static NSString *const kPartial = @"partial"; // NSString
- static NSString *const kPartialValue = @"partialValue"; // NSNumber
- // Help for passing the Expansion info in one shot.
- struct ExpansionInfo {
- // Constant for the whole expansion.
- unichar expressionOperator;
- __unsafe_unretained NSString *joiner;
- BOOL allowReservedInEscape;
- // Update for each variable.
- __unsafe_unretained NSString *explode;
- };
- // Helper just to shorten the lines when needed.
- static NSString *UnescapeString(NSString *str) {
- return [str stringByRemovingPercentEncoding];
- }
- static NSString *EscapeString(NSString *str, BOOL allowReserved) {
- // The spec is a little hard to map onto the charsets, so force
- // reserved bits in/out.
- NSMutableCharacterSet *cs = [[NSCharacterSet URLQueryAllowedCharacterSet] mutableCopy];
- NSString * const kReservedChars = @":/?#[]@!$&'()*+,;=";
- if (allowReserved) {
- [cs addCharactersInString:kReservedChars];
- } else {
- [cs removeCharactersInString:kReservedChars];
- }
- NSString *resultStr = [str stringByAddingPercentEncodingWithAllowedCharacters:cs];
- return resultStr;
- }
- static NSString *StringFromNSNumber(NSNumber *rawValue) {
- NSString *strValue;
- // NSNumber doesn't expose a way to tell if it is holding a BOOL or something
- // else. -[NSNumber objCType] for a BOOL is the same as @encoding(char), but
- // in the 64bit runtine @encoding(BOOL) (or for "bool") won't match that as
- // the 64bit runtime actually has a true boolean type. Instead we reply on
- // checking if the numbers are the CFBoolean constants to force true/value
- // values.
- if ((rawValue == (NSNumber *)kCFBooleanTrue) ||
- (rawValue == (NSNumber *)kCFBooleanFalse)) {
- strValue = (rawValue.boolValue ? @"true" : @"false");
- } else {
- strValue = [rawValue stringValue];
- }
- return strValue;
- }
- @implementation GTLRURITemplate
- #pragma mark Internal Helpers
- + (BOOL)parseExpression:(NSString *)expression
- expressionOperator:(unichar*)outExpressionOperator
- variables:(NSMutableArray **)outVariables
- defaultValues:(NSMutableDictionary **)outDefaultValues {
- // Please see the spec for full details, but here are the basics:
- //
- // URI-Template = *( literals / expression )
- // expression = "{" [ operator ] variable-list "}"
- // variable-list = varspec *( "," varspec )
- // varspec = varname [ modifier ] [ "=" default ]
- // varname = varchar *( varchar / "." )
- // modifier = explode / partial
- // explode = ( "*" / "+" )
- // partial = ( substring / remainder ) offset
- //
- // Examples:
- // http://www.example.com/foo{?query,number}
- // http://maps.com/mapper{?address*}
- // http://directions.org/directions{?from+,to+}
- // http://search.org/query{?terms+=none}
- //
- // http://tools.ietf.org/html/draft-gregorio-uritemplate-04#section-2.2
- // Operator and op-reserve characters
- static NSCharacterSet *operatorSet = nil;
- // http://tools.ietf.org/html/draft-gregorio-uritemplate-04#section-2.4.1
- // Explode characters
- static NSCharacterSet *explodeSet = nil;
- // http://tools.ietf.org/html/draft-gregorio-uritemplate-04#section-2.4.2
- // Partial (prefix/subset) characters
- static NSCharacterSet *partialSet = nil;
- static dispatch_once_t onceToken;
- dispatch_once(&onceToken, ^{
- operatorSet = [NSCharacterSet characterSetWithCharactersInString:@"+./;?|!@"];
- explodeSet = [NSCharacterSet characterSetWithCharactersInString:@"*+"];
- partialSet = [NSCharacterSet characterSetWithCharactersInString:@":^"];
- });
- // http://tools.ietf.org/html/draft-gregorio-uritemplate-04#section-3.3
- // Empty expression inlines the expression.
- if (expression.length == 0) return NO;
- // Pull off any operator.
- *outExpressionOperator = 0;
- unichar firstChar = [expression characterAtIndex:0];
- if ([operatorSet characterIsMember:firstChar]) {
- *outExpressionOperator = firstChar;
- expression = [expression substringFromIndex:1];
- }
- if (expression.length == 0) return NO;
- // Need to find atleast one varspec for the expresssion to be considered
- // valid.
- BOOL gotAVarspec = NO;
- // Split the variable list.
- NSArray *varspecs = [expression componentsSeparatedByString:@","];
- // Extract the defaults, explodes and modifiers from the varspecs.
- *outVariables = [NSMutableArray arrayWithCapacity:varspecs.count];
- for (__strong NSString *varspec in varspecs) {
- NSString *defaultValue = nil;
- if (varspec.length == 0) continue;
- NSMutableDictionary *varInfo =
- [NSMutableDictionary dictionaryWithCapacity:4];
- // Check for a default (foo=bar).
- NSRange range = [varspec rangeOfString:@"="];
- if (range.location != NSNotFound) {
- defaultValue =
- UnescapeString([varspec substringFromIndex:range.location + 1]);
- varspec = [varspec substringToIndex:range.location];
- if (varspec.length == 0) continue;
- }
- // Check for explode (foo*).
- NSUInteger lenLessOne = varspec.length - 1;
- if ([explodeSet characterIsMember:[varspec characterAtIndex:lenLessOne]]) {
- [varInfo setObject:[varspec substringFromIndex:lenLessOne] forKey:kExplode];
- varspec = [varspec substringToIndex:lenLessOne];
- if (varspec.length == 0) continue;
- } else {
- // Check for partial (prefix/suffix) (foo:12).
- range = [varspec rangeOfCharacterFromSet:partialSet];
- if (range.location != NSNotFound) {
- NSString *partialMode = [varspec substringWithRange:range];
- NSString *valueStr = [varspec substringFromIndex:range.location + 1];
- // If there wasn't a value for the partial, ignore it.
- if (valueStr.length > 0) {
- [varInfo setObject:partialMode forKey:kPartial];
- // TODO: Should validate valueStr is just a number...
- [varInfo setObject:[NSNumber numberWithInteger:[valueStr integerValue]]
- forKey:kPartialValue];
- }
- varspec = [varspec substringToIndex:range.location];
- if (varspec.length == 0) continue;
- }
- }
- // Spec allows percent escaping in names, so undo that.
- varspec = UnescapeString(varspec);
- // Save off the cleaned up variable name.
- [varInfo setObject:varspec forKey:kVariable];
- [*outVariables addObject:varInfo];
- gotAVarspec = YES;
- // Now that the variable has been cleaned up, store its default.
- if (defaultValue) {
- if (*outDefaultValues == nil) {
- *outDefaultValues = [NSMutableDictionary dictionary];
- }
- [*outDefaultValues setObject:defaultValue forKey:varspec];
- }
- }
- // All done.
- return gotAVarspec;
- }
- + (NSString *)expandVariables:(NSArray *)variables
- expressionOperator:(unichar)expressionOperator
- values:(NSDictionary *)valueProvider
- defaultValues:(NSMutableDictionary *)defaultValues {
- NSString *prefix = nil;
- struct ExpansionInfo expansionInfo = {
- .expressionOperator = expressionOperator,
- .joiner = nil,
- .allowReservedInEscape = NO,
- .explode = nil,
- };
- switch (expressionOperator) {
- case 0:
- expansionInfo.joiner = @",";
- prefix = @"";
- break;
- case '+':
- expansionInfo.joiner = @",";
- prefix = @"";
- // The reserved character are safe from escaping.
- expansionInfo.allowReservedInEscape = YES;
- break;
- case '.':
- expansionInfo.joiner = @".";
- prefix = @".";
- break;
- case '/':
- expansionInfo.joiner = @"/";
- prefix = @"/";
- break;
- case ';':
- expansionInfo.joiner = @";";
- prefix = @";";
- break;
- case '?':
- expansionInfo.joiner = @"&";
- prefix = @"?";
- break;
- default:
- [NSException raise:@"GTLRURITemplateUnsupported"
- format:@"Unknown expression operator '%C'", expressionOperator];
- break;
- }
- NSMutableArray *results = [NSMutableArray arrayWithCapacity:variables.count];
- for (NSDictionary *varInfo in variables) {
- NSString *variable = [varInfo objectForKey:kVariable];
- expansionInfo.explode = [varInfo objectForKey:kExplode];
- // Look up the variable value.
- id rawValue = [valueProvider objectForKey:variable];
- // If the value is an empty array or dictionary, the default is still used.
- if (([rawValue isKindOfClass:[NSArray class]]
- || [rawValue isKindOfClass:[NSDictionary class]])
- && ((NSArray *)rawValue).count == 0) {
- rawValue = nil;
- }
- // Got nothing? Check defaults.
- if (rawValue == nil) {
- rawValue = [defaultValues objectForKey:variable];
- }
- // If we didn't get any value, on to the next thing.
- if (!rawValue) {
- continue;
- }
- // Time do to the work...
- NSString *result = nil;
- if ([rawValue isKindOfClass:[NSString class]]) {
- result = [self expandString:rawValue
- variableName:variable
- expansionInfo:&expansionInfo];
- } else if ([rawValue isKindOfClass:[NSNumber class]]) {
- // Turn the number into a string and send it on its way.
- NSString *strValue = StringFromNSNumber(rawValue);
- result = [self expandString:strValue
- variableName:variable
- expansionInfo:&expansionInfo];
- } else if ([rawValue isKindOfClass:[NSArray class]]) {
- result = [self expandArray:rawValue
- variableName:variable
- expansionInfo:&expansionInfo];
- } else if ([rawValue isKindOfClass:[NSDictionary class]]) {
- result = [self expandDictionary:rawValue
- variableName:variable
- expansionInfo:&expansionInfo];
- } else {
- [NSException raise:@"GTLRURITemplateUnsupported"
- format:@"Variable returned unsupported type (%@)",
- NSStringFromClass([rawValue class])];
- }
- // Did it generate anything?
- if (result == nil)
- continue;
- // Apply partial.
- // Defaults should get partial applied?
- // ( http://tools.ietf.org/html/draft-gregorio-uritemplate-04#section-2.5 )
- NSString *partial = [varInfo objectForKey:kPartial];
- if (partial.length > 0) {
- [NSException raise:@"GTLRURITemplateUnsupported"
- format:@"Unsupported partial on expansion %@", partial];
- }
- // Add the result
- [results addObject:result];
- }
- // Join and add any needed prefix.
- NSString *joinedResults =
- [results componentsJoinedByString:expansionInfo.joiner];
- if ((prefix.length > 0) && (joinedResults.length > 0)) {
- return [prefix stringByAppendingString:joinedResults];
- }
- return joinedResults;
- }
- + (NSString *)expandString:(NSString *)valueStr
- variableName:(NSString *)variableName
- expansionInfo:(struct ExpansionInfo *)expansionInfo {
- NSString *escapedValue =
- EscapeString(valueStr, expansionInfo->allowReservedInEscape);
- switch (expansionInfo->expressionOperator) {
- case ';':
- case '?':
- if (valueStr.length > 0) {
- return [NSString stringWithFormat:@"%@=%@", variableName, escapedValue];
- }
- return variableName;
- default:
- return escapedValue;
- }
- }
- + (NSString *)expandArray:(NSArray *)valueArray
- variableName:(NSString *)variableName
- expansionInfo:(struct ExpansionInfo *)expansionInfo {
- NSMutableArray *results = [NSMutableArray arrayWithCapacity:valueArray.count];
- // When joining variable with value, use "var.val" except for 'path' and
- // 'form' style expression, use 'var=val' then.
- char variableValueJoiner = '.';
- unichar expressionOperator = expansionInfo->expressionOperator;
- if ((expressionOperator == ';') || (expressionOperator == '?')) {
- variableValueJoiner = '=';
- }
- // Loop over the values.
- for (id rawValue in valueArray) {
- NSString *value;
- if ([rawValue isKindOfClass:[NSNumber class]]) {
- value = StringFromNSNumber((id)rawValue);
- } else if ([rawValue isKindOfClass:[NSString class]]) {
- value = rawValue;
- } else {
- [NSException raise:@"GTLRURITemplateUnsupported"
- format:@"Variable '%@' returned NSArray with unsupported type (%@), array: %@",
- variableName, NSStringFromClass([rawValue class]), valueArray];
- }
- // Escape it.
- value = EscapeString(value, expansionInfo->allowReservedInEscape);
- // Should variable names be used?
- if ([expansionInfo->explode isEqual:@"+"]) {
- value = [NSString stringWithFormat:@"%@%c%@",
- variableName, variableValueJoiner, value];
- }
- [results addObject:value];
- }
- if (results.count > 0) {
- // Use the default joiner unless there was no explode request, then a list
- // always gets comma seperated.
- NSString *joiner = expansionInfo->joiner;
- if (expansionInfo->explode == nil) {
- joiner = @",";
- }
- // Join the values.
- NSString *joined = [results componentsJoinedByString:joiner];
- // 'form' style without an explode gets the variable name set to the
- // joined list of values.
- if ((expressionOperator == '?') && (expansionInfo->explode == nil)) {
- return [NSString stringWithFormat:@"%@=%@", variableName, joined];
- }
- return joined;
- }
- return nil;
- }
- + (NSString *)expandDictionary:(NSDictionary *)valueDict
- variableName:(NSString *)variableName
- expansionInfo:(struct ExpansionInfo *)expansionInfo {
- NSMutableArray *results = [NSMutableArray arrayWithCapacity:valueDict.count];
- // When joining variable with value:
- // - Default to the joiner...
- // - No explode, always comma...
- // - For 'path' and 'form' style expression, use 'var=val'.
- NSString *keyValueJoiner = expansionInfo->joiner;
- unichar expressionOperator = expansionInfo->expressionOperator;
- if (expansionInfo->explode == nil) {
- keyValueJoiner = @",";
- } else if ((expressionOperator == ';') || (expressionOperator == '?')) {
- keyValueJoiner = @"=";
- }
- // Loop over the sorted keys.
- NSArray *sortedKeys = [valueDict.allKeys sortedArrayUsingSelector:@selector(compare:)];
- for (__strong NSString *key in sortedKeys) {
- NSString *value = [valueDict objectForKey:key];
- // Escape them.
- key = EscapeString(key, expansionInfo->allowReservedInEscape);
- value = EscapeString(value, expansionInfo->allowReservedInEscape);
- // Should variable names be used?
- if ([expansionInfo->explode isEqual:@"+"]) {
- key = [NSString stringWithFormat:@"%@.%@", variableName, key];
- }
- if ((expressionOperator == '?' || expressionOperator == ';')
- && (value.length == 0)) {
- [results addObject:key];
- } else {
- NSString *pair = [NSString stringWithFormat:@"%@%@%@",
- key, keyValueJoiner, value];
- [results addObject:pair];
- }
- }
- if (results.count) {
- // Use the default joiner unless there was no explode request, then a list
- // always gets comma seperated.
- NSString *joiner = expansionInfo->joiner;
- if (expansionInfo->explode == nil) {
- joiner = @",";
- }
- // Join the values.
- NSString *joined = [results componentsJoinedByString:joiner];
- // 'form' style without an explode gets the variable name set to the
- // joined list of values.
- if ((expressionOperator == '?') && (expansionInfo->explode == nil)) {
- return [NSString stringWithFormat:@"%@=%@", variableName, joined];
- }
- return joined;
- }
- return nil;
- }
- #pragma mark Public API
- + (NSString *)expandTemplate:(NSString *)uriTemplate
- values:(NSDictionary *)valueProvider {
- NSMutableString *result =
- [NSMutableString stringWithCapacity:uriTemplate.length];
- NSScanner *scanner = [NSScanner scannerWithString:uriTemplate];
- [scanner setCharactersToBeSkipped:nil];
- // Defaults have to live through the full evaluation, so if any are encoured
- // they are reused throughout the expansion calls.
- NSMutableDictionary *defaultValues = nil;
- // Pull out the expressions for processing.
- while (![scanner isAtEnd]) {
- NSString *skipped = nil;
- // Find the next '{'.
- if ([scanner scanUpToString:@"{" intoString:&skipped]) {
- // Add anything before it to the result.
- [result appendString:skipped];
- }
- // Advance over the '{'.
- [scanner scanString:@"{" intoString:nil];
- // Collect the expression.
- NSString *expression = nil;
- if ([scanner scanUpToString:@"}" intoString:&expression]) {
- // Collect the trailing '}' on the expression.
- BOOL hasTrailingBrace = [scanner scanString:@"}" intoString:nil];
- // Parse the expression.
- NSMutableArray *variables = nil;
- unichar expressionOperator = 0;
- if ([self parseExpression:expression
- expressionOperator:&expressionOperator
- variables:&variables
- defaultValues:&defaultValues]) {
- // Do the expansion.
- NSString *substitution = [self expandVariables:variables
- expressionOperator:expressionOperator
- values:valueProvider
- defaultValues:defaultValues];
- if (substitution) {
- [result appendString:substitution];
- }
- } else {
- // Failed to parse, add the raw expression to the output.
- if (hasTrailingBrace) {
- [result appendFormat:@"{%@}", expression];
- } else {
- [result appendFormat:@"{%@", expression];
- }
- }
- } else if (![scanner isAtEnd]) {
- // Empty expression ('{}'). Copy over the opening brace and the trailing
- // one will be copied by the next cycle of the loop.
- [result appendString:@"{"];
- }
- }
- return result;
- }
- @end
|