Transcript
Page 1: MTC: Hallo Welt – RESTful Services per RestKit ansprechen

Hallo Welt - RESTful Services per RestkitMichael Kotten | open knowledge GmbH @michaelkotten

@_openKnowledge

Page 2: MTC: Hallo Welt – RESTful Services per RestKit ansprechen

MTC2014 RESTful Services per RestKit

Webservices sind überall

Page 3: MTC: Hallo Welt – RESTful Services per RestKit ansprechen

MTC2014 RESTful Services per RestKit

Twitter JSON Beispiel

{ "id": 501673189681135616, "created_at": "Tue Aug 19 10:13:03 +0000 2014", "in_reply_to_screen_name": null, "in_reply_to_status_id": null, "in_reply_to_user_id": null, "retweet_count": 0, "retweeted": false, "text": "#Swift available for everybody now.", "user": { "id": 78700609, "name": "Michael Kotten", "screen_name": "michaelkotten", "created_at": "Wed Sep 30 20:28:36 +0000 2009", "followers_count": 13, "friends_count": 29, "statuses_count": 27 } }

Page 4: MTC: Hallo Welt – RESTful Services per RestKit ansprechen

MTC2014 RESTful Services per RestKit

Twitter JSON Beispiel

{ "id": 501673189681135616, "created_at": "Tue Aug 19 10:13:03 +0000 2014", "in_reply_to_screen_name": null, "in_reply_to_status_id": null, "in_reply_to_user_id": null, "retweet_count": 0, "retweeted": false, "text": "#Swift available for everybody now.", "user": { "id": 78700609, "name": "Michael Kotten", "screen_name": "michaelkotten", "created_at": "Wed Sep 30 20:28:36 +0000 2009", "followers_count": 13, "friends_count": 29, "statuses_count": 27 } }

Follow me

Page 5: MTC: Hallo Welt – RESTful Services per RestKit ansprechen
Page 6: MTC: Hallo Welt – RESTful Services per RestKit ansprechen

MTC2014 RESTful Services per RestKit

Achtung, BETA!

Page 7: MTC: Hallo Welt – RESTful Services per RestKit ansprechen

MTC2014 RESTful Services per RestKit

RESTful Services per NSUrlSession

var sessionConfig = NSURLSessionConfiguration.defaultSessionConfiguration() sessionConfig.HTTPAdditionalHeaders = ["Accept" : "application/json"] !var session = NSURLSession(configuration: sessionConfig) !var task = session.dataTaskWithURL(url, completionHandler: { (data, response, error) in var json = NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.AllowFragments, error: nil) as? Dictionary<String, AnyObject> var tweet = json[„text“] ... }) task.resume()

Page 8: MTC: Hallo Welt – RESTful Services per RestKit ansprechen

MTC2014 RESTful Services per RestKit

RESTful Services richtig

Aber wie denn jetzt?

Page 9: MTC: Hallo Welt – RESTful Services per RestKit ansprechen

MTC2014 RESTful Services per RestKit

RestKit

Page 10: MTC: Hallo Welt – RESTful Services per RestKit ansprechen

MTC2014 RESTful Services per RestKit

– RestKit.org

„RestKit is a RESTful Object Mapping Framework for iOS and OSX.“

Was ist das?

Page 11: MTC: Hallo Welt – RESTful Services per RestKit ansprechen

MTC2014 RESTful Services per RestKit

Warum

RestKit

?

Page 12: MTC: Hallo Welt – RESTful Services per RestKit ansprechen

RESTful Services per RestKitMTC2014

Features

Easy switching environments

Core Data support

A simple, high level Network layerObject Mapping

Pluggable parsing layer

Page 13: MTC: Hallo Welt – RESTful Services per RestKit ansprechen

RESTful Services per RestKitMTC2014

Installation per CocoaPods

‣ CocoaPods installieren 1. gem  install  cocoapods  2. pod  setup

‣ PodFile erzeugen platform  :ios,  '7.1'      pod  'RestKit',  '~>    0.23.2'  !$  pod  install  $  open  MyProject.xcworkspace

Page 14: MTC: Hallo Welt – RESTful Services per RestKit ansprechen

MTC2014 RESTful Services per RestKit

Objective-C importieren

Page 15: MTC: Hallo Welt – RESTful Services per RestKit ansprechen

MTC2014 RESTful Services per RestKit

Objective-C importieren

YES, please!

Page 16: MTC: Hallo Welt – RESTful Services per RestKit ansprechen

MTC2014 RESTful Services per RestKit

MyRestKitApp-Bridging-Header.h

// // Use this file to import your target's public headers that // you would like to expose to Swift. // #import <Foundation/Foundation.h> #import <CoreData/CoreData.h> #import <RestKit/RestKit.h> #import <RestKit/CoreData.h>

Page 17: MTC: Hallo Welt – RESTful Services per RestKit ansprechen

MTC2014 RESTful Services per RestKit

Setup

let baseUrl = NSURL(string: "https://twitter.com") !// Initialize RestKit let objectManager = RKObjectManager(baseURL: baseUrl)

Page 18: MTC: Hallo Welt – RESTful Services per RestKit ansprechen

MTC2014 RESTful Services per RestKit

Network Indicator

AFNetworkActivityIndicatorManager.sharedManager().enabled = true

Page 19: MTC: Hallo Welt – RESTful Services per RestKit ansprechen

MTC2014 RESTful Services per RestKit

Object Mapping

Object Mapping

Page 20: MTC: Hallo Welt – RESTful Services per RestKit ansprechen

MTC2014 RESTful Services per RestKit

– RestKit.org

„Object mapping is the process of taking a representation of data in one form and

transforming it into another“

Object Mapping

Page 21: MTC: Hallo Welt – RESTful Services per RestKit ansprechen

RESTful Services per RestKitMTC2014

Object Mapping - Features

‣Mapping per Key-Value Coding ‣ automatische Transformation in Ziel-Typ ‣ erweiterbar

‣ Relationship-Mapping ‣ to-one und to-many ‣ Rekursiv

Page 22: MTC: Hallo Welt – RESTful Services per RestKit ansprechen

RESTful Services per RestKitMTC2014

Object Mapping

‣ RKObjectMapping ‣ Definiert die „Regeln“ für ein Mapping ‣ Property Mappings für Attribute und

Beziehungen

Page 23: MTC: Hallo Welt – RESTful Services per RestKit ansprechen

MTC2014 RESTful Services per RestKit

Object Mapping

"user": { "id": 78700609, "name": "Michael Kotten", "screen_name": "michaelkotten" }

class User : NSObject { var userId: NSNumber? var name: String? var screenName: String? }

Page 24: MTC: Hallo Welt – RESTful Services per RestKit ansprechen

MTC2014 RESTful Services per RestKit

Object Mapping

"user": { "id": 78700609, "name": "Michael Kotten", "screen_name": "michaelkotten" }

class User : NSObject { var userId: NSNumber? var name: String? var screenName: String? }

Page 25: MTC: Hallo Welt – RESTful Services per RestKit ansprechen

MTC2014 RESTful Services per RestKit

Object Mapping

"user": { "id": 78700609, "name": "Michael Kotten", "screen_name": "michaelkotten" }

class User : NSObject { var userId: NSNumber? var name: String? var screenName: String? }

var userMapping = RKObjectMapping(forClass: User.self) userMapping.addAttributeMappingsFromDictionary([ "id" : "userId", "name" : "name", "screen_name" : "screenName"])

Page 26: MTC: Hallo Welt – RESTful Services per RestKit ansprechen

MTC2014 RESTful Services per RestKit

Object Mapping

"user": { "id": 78700609, "name": "Michael Kotten", "screen_name": "michaelkotten" }

class User : NSObject { var userId: NSNumber? var name: String? var screenName: String? }

var userMapping = RKObjectMapping(forClass: User.self) userMapping.addAttributeMappingsFromDictionary([ "id" : "userId", "name" : "name", "screen_name" : "screenName"])

Page 27: MTC: Hallo Welt – RESTful Services per RestKit ansprechen

MTC2014 RESTful Services per RestKit

Swift und RestKit

Mapping Klassen müssen von NSObject

ableiten!

Page 28: MTC: Hallo Welt – RESTful Services per RestKit ansprechen

MTC2014 RESTful Services per RestKit

Object Mapping

class Tweet : NSObject { var tweetId: NSNumber? var createdAt: NSDate? var text: String? var inReplyToScreenName: String? var user: User? }

{ "id": 501673189681135616, "created_at": "Tue Aug 19 10:13:03 +0000 2014", "in_reply_to_screen_name": null, "text": "#Swift available for everybody now.“, "user": { "id": 78700609, "name": "Michael Kotten", "screen_name": "michaelkotten" } }

Page 29: MTC: Hallo Welt – RESTful Services per RestKit ansprechen

MTC2014 RESTful Services per RestKit

Object Mapping

class Tweet : NSObject { var tweetId: NSNumber? var createdAt: NSDate? var text: String? var inReplyToScreenName: String? var user: User? }

{ "id": 501673189681135616, "created_at": "Tue Aug 19 10:13:03 +0000 2014", "in_reply_to_screen_name": null, "text": "#Swift available for everybody now.“, "user": { "id": 78700609, "name": "Michael Kotten", "screen_name": "michaelkotten" } }

?

Page 30: MTC: Hallo Welt – RESTful Services per RestKit ansprechen

MTC2014 RESTful Services per RestKit

Object Mapping

let tweetMapping = RKObjectMapping(forClass: Tweet.self) tweetMapping.addAttributeMappingsFromDictionary([ "id" : "tweetId", "created_at" : "createdAt", "text" : "text", "in_reply_to_screen_name" : "inReplyToScreenName"]) !let relationshipMapping = RKRelationshipMapping(fromKeyPath: "user", toKeyPath: "user", withMapping: userMapping) !tweetMapping.addPropertyMapping(relationshipMapping)

Page 31: MTC: Hallo Welt – RESTful Services per RestKit ansprechen

RESTful Services per RestKitMTC2014

Response Descriptor

‣ RKResponseDescriptor ‣ Object Mapping ‣ URL Pattern ‣ Key Path ‣ HTTP Status Codes

Page 32: MTC: Hallo Welt – RESTful Services per RestKit ansprechen

MTC2014 RESTful Services per RestKit

Response Descriptor

let responseDescriptor = RKResponseDescriptor( mapping: tweetMapping, method: RKRequestMethod.GET, pathPattern: "/status/user_timeline/:username", keyPath: nil, statusCodes: RKStatusCodeIndexSetForClass( UInt(RKStatusCodeClassSuccessful))) !objectManager.addResponseDescriptor(responseDescriptor)

Page 33: MTC: Hallo Welt – RESTful Services per RestKit ansprechen

MTC2014 RESTful Services per RestKit

GETting Objects

objectManager.getObjectsAtPath( "/status/user_timeline/michaelkotten", parameters: nil, success: { (operation: RKObjectRequestOperation!, mappingResult: RKMappingResult!) -> () in self.tweets = mappingResult.array() as [Tweet] println("\(self.tweets.count) tweets loaded") self.tableView.reloadData() }, failure: { (operation: RKObjectRequestOperation!, error: NSError!) -> () in println("Error loading tweets: \(error)") })

Page 34: MTC: Hallo Welt – RESTful Services per RestKit ansprechen

RESTful Services per RestKitMTC2014

GETting Objects

‣ GET Request per RKObjectRequestOperation

‣ Response verarbeiten per RKResponseMapperOperation ‣ Content-Type bestimmen ‣ application/json -> NSJSONSerialization

‣ RKResponseDescriptor finden ‣ URL Pattern ‣ Key Path ‣ Status Codes

Page 35: MTC: Hallo Welt – RESTful Services per RestKit ansprechen

RESTful Services per RestKitMTC2014

GETting Objects

‣Mapping ausführen per RKMappingOperation ‣ valueForKeyPath("text") ‣ Transformation für NSDate etc. ‣ setValue("#Swift available for every…", forKeyPath: "text")

!

‣ Ergebnis als RKMappingResult ‣ Enthält Mapping Objekte

Page 36: MTC: Hallo Welt – RESTful Services per RestKit ansprechen

MTC2014 RESTful Services per RestKit

GETting Objects

Page 37: MTC: Hallo Welt – RESTful Services per RestKit ansprechen

„That‘s the way, we like it!“

Page 38: MTC: Hallo Welt – RESTful Services per RestKit ansprechen

MTC2014 RESTful Services per RestKit

Persistierung

‣ POST, PATCH/PUT und DELETE ‣ Inverse Mapping ‣ RKRequestDescriptor

Page 39: MTC: Hallo Welt – RESTful Services per RestKit ansprechen

MTC2014 RESTful Services per RestKit

Persistierung

var inverseMapping = tweetMapping.inverseMapping() var requestDescriptor = RKRequestDescriptor( mapping: inverseMapping, objectClass: Tweet.self, rootKeyPath: "status", method: RKRequestMethod.POST) objectManager.addRequestDescriptor(requestDescriptor)

Page 40: MTC: Hallo Welt – RESTful Services per RestKit ansprechen

MTC2014 RESTful Services per RestKit

POSTing Objects

var tweet = Tweet() tweet.text = "This tweet was posted using #RestKit!" objectManager.postObject( tweet, path: "statuses/update.json", parameters: nil, success: { (operation: RKObjectRequestOperation!, mappingResult: RKMappingResult!) in println("tweet successfully posted") }, failure: { (operation: RKObjectRequestOperation!, error: NSError!) in println("tweet post failed!") })

Page 41: MTC: Hallo Welt – RESTful Services per RestKit ansprechen

RESTful Services per RestKitMTC2014

POSTing Objects

‣ POST Request per RKObjectRequestOperation ‣ erzeugt NSURLRequest für URL

‣ passenden RKRequestDescriptor finden ‣ inverse Mapping

‣ Transformation nach NSDictionary !

‣ Ergebnis wieder als RKMappingResult

Page 42: MTC: Hallo Welt – RESTful Services per RestKit ansprechen

RESTful Services per RestKitMTC2014

PUT, PATCH und DELETE

‣ Übrige CRUD Operation analog: ‣ objectManager.putObject(…) ‣ objectManager.patchObject(…) ‣ objectManager.deleteObject(…)

Page 43: MTC: Hallo Welt – RESTful Services per RestKit ansprechen

RESTful Services per RestKitMTC2014

Routing

‣ Zentrale Erzeugung von Urls ‣minimiert Verwendung von Path Patterns ‣ Drei Typen von RKRoute ‣ Named Routes ‣ Class Routes ‣ Relationship Routes

Page 44: MTC: Hallo Welt – RESTful Services per RestKit ansprechen

MTC2014 RESTful Services per RestKit

Named Routes

objectManager.router.routeSet.addRoute( RKRoute(name: "myTimeline", pathPattern: "/status/user_timeline/michaelkotten", method: RKRequestMethod.GET) )

Page 45: MTC: Hallo Welt – RESTful Services per RestKit ansprechen

MTC2014 RESTful Services per RestKit

Named Routes

objectManager.router.routeSet.addRoute( RKRoute(name: "myTimeline", pathPattern: "/status/user_timeline/michaelkotten", method: RKRequestMethod.GET) )

objectManager.getObjectsAtPathForRouteNamed("myTimeline", object: nil, parameters: nil, success: { (operation: RKObjectRequestOperation!, mappingResult: RKMappingResult!) -> () in self.tweets = mappingResult.array() as [Tweet] println("\(self.tweets.count) tweets loaded") self.tableView.reloadData() }, failure: { (operation: RKObjectRequestOperation!, error: NSError!) -> () in println("Error loading tweets: \(error)") })

Page 46: MTC: Hallo Welt – RESTful Services per RestKit ansprechen

MTC2014 RESTful Services per RestKit

objectManager.router.routeSet.addRoute( RKRoute(class: Tweet.self, pathPattern: "/statuses/update.json", method: RKRequestMethod.POST))

Class Routes

Page 47: MTC: Hallo Welt – RESTful Services per RestKit ansprechen

MTC2014 RESTful Services per RestKit

objectManager.router.routeSet.addRoute( RKRoute(class: Tweet.self, pathPattern: "/statuses/update.json", method: RKRequestMethod.POST))

Class Routes

let tweet = Tweet() tweet.text = "This tweet was posted using #RestKit!" objectManager.postObject(user, path: nil, parameters: nil, success: { (operation: RKObjectRequestOperation!, mappingResult: RKMappingResult!) -> () in println("tweet successfully posted") }, failure: { (operation: RKObjectRequestOperation!, error: NSError!) -> () in println("tweet post failed!") })

Page 48: MTC: Hallo Welt – RESTful Services per RestKit ansprechen

MTC2014 RESTful Services per RestKit

objectManager.router.routeSet.addRoute( RKRoute(class: Tweet.self, pathPattern: "/statuses/update.json", method: RKRequestMethod.POST))

Class Routes

let tweet = Tweet() tweet.text = "This tweet was posted using #RestKit!" objectManager.postObject(user, path: nil, parameters: nil, success: { (operation: RKObjectRequestOperation!, mappingResult: RKMappingResult!) -> () in println("tweet successfully posted") }, failure: { (operation: RKObjectRequestOperation!, error: NSError!) -> () in println("tweet post failed!") })

Page 49: MTC: Hallo Welt – RESTful Services per RestKit ansprechen

MTC2014 RESTful Services per RestKit

objectManager.router.routeSet.addRoute( RKRoute(`class`: Tweet.self, pathPattern: "/statuses/update.json", method: RKRequestMethod.POST))

Class Routes

let tweet = Tweet() tweet.text = "This tweet was posted using #RestKit!" objectManager.postObject(user, path: nil, parameters: nil, success: { (operation: RKObjectRequestOperation!, mappingResult: RKMappingResult!) -> () in println("tweet successfully posted") }, failure: { (operation: RKObjectRequestOperation!, error: NSError!) -> () in println("tweet post failed!") })

Page 50: MTC: Hallo Welt – RESTful Services per RestKit ansprechen

MTC2014 RESTful Services per RestKit

Relationship Routes

var responseDescriptor = RKResponseDescriptor( mapping: userMapping, method: RKRequestMethod.GET, pathPattern: "/status/:tweetId/retweeted_by", keyPath: nil, statusCodes: NSIndexSet(index: 200)) objectManager.addResponseDescriptor(responseDescriptor) !objectManager.router.routeSet.addRoute( RKRoute(relationshipName: „retweeted_by", objectClass: Tweet.self, pathPattern: "/status/:tweetId/retweeted_by", method: RKRequestMethod.GET))

Page 51: MTC: Hallo Welt – RESTful Services per RestKit ansprechen

MTC2014 RESTful Services per RestKit

Relationship Routes

var responseDescriptor = RKResponseDescriptor( mapping: userMapping, method: RKRequestMethod.GET, pathPattern: "/status/:tweetId/retweeted_by", keyPath: nil, statusCodes: NSIndexSet(index: 200)) objectManager.addResponseDescriptor(responseDescriptor) !objectManager.router.routeSet.addRoute( RKRoute(relationshipName: „retweeted_by", objectClass: Tweet.self, pathPattern: "/status/:tweetId/retweeted_by", method: RKRequestMethod.GET))

var tweet = Tweet() tweet.tweetId = 4711 objectManager.getObjectsAtPathForRelationship("retweeted_by", ofObject: tweet, parameters: nil, success: { (operation: RKObjectRequestOperation!, mappingResult: RKMappingResult!) -> () in var users = mappingResult.array() as [User] println("\(users.count) retweets loaded") }, failure: { (operation: RKObjectRequestOperation!, error: NSError!) -> () in println("Error loading retweets: \(error)") })

Page 52: MTC: Hallo Welt – RESTful Services per RestKit ansprechen

MTC2014 RESTful Services per RestKit

Core Data

Page 53: MTC: Hallo Welt – RESTful Services per RestKit ansprechen

MTC2014 RESTful Services per RestKit

Managed Object Store

Core Data

Page 54: MTC: Hallo Welt – RESTful Services per RestKit ansprechen

MTC2014 RESTful Services per RestKit

Managed Object Store

var objectManager = RKObjectManager(baseURL: baseUrl) !let managedObjectStore = RKManagedObjectStore( persistentStoreCoordinator: persistentStoreCoordinator) objectManager.managedObjectStore = managedObjectStore

Page 55: MTC: Hallo Welt – RESTful Services per RestKit ansprechen

MTC2014 RESTful Services per RestKit

Mapping ändern

Core Data

Page 56: MTC: Hallo Welt – RESTful Services per RestKit ansprechen

MTC2014 RESTful Services per RestKit

Mapping ändern

class User : NSObject { var userId: NSNumber? var name: String? var screenName: String? }

Page 57: MTC: Hallo Welt – RESTful Services per RestKit ansprechen

MTC2014 RESTful Services per RestKit

Mapping ändern

@objc(User) class User : NSManagedObject { @NSManaged var userId: NSNumber? @NSManaged var name: String? @NSManaged var screenName: String?

Page 58: MTC: Hallo Welt – RESTful Services per RestKit ansprechen

MTC2014 RESTful Services per RestKit

Mapping ändern

@objc(User) class User : NSManagedObject { @NSManaged var userId: NSNumber? @NSManaged var name: String? @NSManaged var screenName: String? }

Page 59: MTC: Hallo Welt – RESTful Services per RestKit ansprechen

MTC2014 RESTful Services per RestKit

Mapping ändern

@objc(User) class User : NSManagedObject { @NSManaged var userId: NSNumber? @NSManaged var name: String? @NSManaged var screenName: String? }

Page 60: MTC: Hallo Welt – RESTful Services per RestKit ansprechen

MTC2014 RESTful Services per RestKit

Mapping ändern

@objc(User) class User : NSManagedObject { @NSManaged var userId: NSNumber? @NSManaged var name: String? @NSManaged var screenName: String? }

Page 61: MTC: Hallo Welt – RESTful Services per RestKit ansprechen

MTC2014 RESTful Services per RestKit

Mapping ändern

var userMapping = RKObjectMapping(forClass: User.self) userMapping.addAttributeMappingsFromDictionary([ "id" : "userId", "name" : "name", "screen_name" : "screenName"])

Page 62: MTC: Hallo Welt – RESTful Services per RestKit ansprechen

MTC2014 RESTful Services per RestKit

Mapping ändern

var userMapping = RKObjectMapping(forClass: User.self) userMapping.addAttributeMappingsFromDictionary([ "id" : "userId", "name" : "name", "screen_name" : "screenName"])

var userMapping = RKEntityMapping(forEntityForName: "User", inManagedObjectStore: managedObjectStore) userMapping.identificationAttributes = ["userId"] userMapping.addAttributeMappingsFromDictionary([ "id" : "userId", "name" : "name", "screen_name" : "screenName"])

Page 63: MTC: Hallo Welt – RESTful Services per RestKit ansprechen

MTC2014 RESTful Services per RestKit

Core Data Model

Core Data

Page 64: MTC: Hallo Welt – RESTful Services per RestKit ansprechen

MTC2014 RESTful Services per RestKit

Core Data Model

Page 65: MTC: Hallo Welt – RESTful Services per RestKit ansprechen

MTC2014 RESTful Services per RestKit

Managed Object Context

Core Data

Page 66: MTC: Hallo Welt – RESTful Services per RestKit ansprechen

MTC2014 RESTful Services per RestKit

Managed Object Context

managedObjectStore.createManagedObjectContexts()

Page 67: MTC: Hallo Welt – RESTful Services per RestKit ansprechen

MTC2014 RESTful Services per RestKit

Objekte erzeugen

Core Data

Page 68: MTC: Hallo Welt – RESTful Services per RestKit ansprechen

MTC2014 RESTful Services per RestKit

Objekte erzeugen

let managedObjectContext = objectManager.managedObjectStore.mainQueueManagedObjectContext !var description = NSEntityDescription.entityForName(„Tweet", inManagedObjectContext:managedObjectContext) !var tweet = Tweet(entity: description, insertIntoManagedObjectContext: managedObjectContext)

Page 69: MTC: Hallo Welt – RESTful Services per RestKit ansprechen

MTC2014 RESTful Services per RestKit

Fazit

Abspann

Page 70: MTC: Hallo Welt – RESTful Services per RestKit ansprechen

Lohnt sich das denn?

Page 71: MTC: Hallo Welt – RESTful Services per RestKit ansprechen

RESTful Services per RestKitMTC2014

Fazit

‣ Bietet viele Features ‣ Relativ einfache API ‣ Aktive Entwicklung & Community ‣ Für einfache Webservices oversized ‣ Abhängigkeit muss abgewogen werden

Page 72: MTC: Hallo Welt – RESTful Services per RestKit ansprechen

MTC2014 RESTful Services per RestKit

https://github.com/RestKit/RestKit

Get it now!

Page 73: MTC: Hallo Welt – RESTful Services per RestKit ansprechen

Hallo Welt - RESTful Services per RestkitMichael Kotten | open knowledge GmbH @michaelkotten

@_openKnowledge


Top Related