mitsu's techlog

Customer Reliability Engineerになりました。備忘録も兼ねて技術ネタを適当に。技術以外はこっち→http://mitsu9.hatenablog.com/

SwiftでWunderlistのAPI使ってみた

Todoistに続き、WunderlistからもAPIを使ってデータを取ってみました。 これからはGitHubにサンプルも上げていこうと思うので、もしコードをみたい方がいればそちらも見てみてください。

GitHubのサンプルはこちら

全体的な流れはTodoistの時と基本は全く同じなのですが、Wunderlistの方がドキュメントはしっかりしており、パラメータも単にURLにつけるだけではなくHTTPのbodyやheaderにつけないといけなかったりしました。 しかし、パラメータのつけ方だけ気をつければ基本的には大丈夫だと思います!

参考サイト

Wunderlist API Documentation

事前準備

毎回のことですが、こちらからClient_IDとClient_Secretを取得します。

認証

認証もOAuthなので基本的にやることは同じです。

STEP1

リクエストを送信します。エンドポイントは以下のとおりです。 https://www.wunderlist.com/oauth/authorize?client_id=ID&redirect_uri=URL&state=RANDOM

パラメータは3つ

  • client_id : クライアントID
  • redirect_uri : リダイレクトURL
  • state : ランダムな文字列

Todoistのときはredirect_uriがoptionalでwebで設定できたのですが、Wunderlistではrequiredになっていました。

こちらは前回同様WebViewを利用してリクエストを送信しています。 以下のように認証用にWebViewを持つだけのシンプルなVCを作って実装しました。

class Wunderlist {
    private struct Info {
        static let client_id = "your_client_id"
        static let client_secret = "your_client_secret"
        static let redirect_uri = "https://localhost/"
        static let state = "state"
        
        static var code: String? = nil
        static var access_token: String? = nil {
            didSet {
                let ud = NSUserDefaults.standardUserDefaults()
                ud.setValue(access_token, forKey: Wunderlist.accessTokenPath)
            }
        }
    }

    private let authRequestURL = "https://www.wunderlist.com/oauth/authorize?client_id=\(Info.client_id)&redirect_uri=\(Info.redirect_uri)&state=\(Info.state)"

    // --<略>--
}

class WunderlistAuthController: UIViewController, UIWebViewDelegate {
    override func viewDidLoad() {
        super.viewDidLoad()
    
        webView = UIWebView(frame: CGRectMake(0, 0, UIScreen.mainScreen().bounds.width, UIScreen.mainScreen().bounds.height))
        webView!.delegate = self
        view.addSubview(webView!)
        if let url = NSURL(string: Wunderlist.sharedInstance.authRequestURL) {
            webView!.loadRequest(NSURLRequest(URL: url))
        }
    }
}

STEP2

ユーザーが認証・許可をするとリダイレクトしてくれるので、WebViewでそれを拾って、access_tokenを取得するためのリクエストを送信します。

まず、URLからcodeを取得します。 shouldStartLoadWithRequestの中でリダイレクト先と一致していた場合、codeを取得するようにしました。

func webView(webView: UIWebView, shouldStartLoadWithRequest request: NSURLRequest, navigationType: UIWebViewNavigationType) -> Bool {
    if let url = request.URL {
        if url.host == Wunderlist.sharedInstance.redirectHost {
            let query = url.query!
            // query forms "state=state&code=<code>"
            let code = query.substringFromIndex(advance(query.startIndex, 17))
            println("query: \(query),code: \(code)")
            Wunderlist.sharedInstance.getAccessToken(code)
            return true
        }
    }
    return true
}

次にリクエストをPOSTします。

パラメータは3つ

  • client_id: クライアントID
  • client_secret: クライアントsecret
  • code: 先ほど取得したcode

このパラメータなのですが、Todoistの時とは違い、HTTPBodyに記述します。 なのでコードは以下のような感じになります。

private let accessTokenRequestURL = "https://www.wunderlist.com/oauth/access_token"
private var accessTokenHTTPBody: String? {
    get {
        if let code = Info.code {
            return "client_id=\(Info.client_id)&client_secret=\(Info.client_secret)&code=\(code)"
        }
        return nil
    }
}

private func getAccessToken(code: String) {
    Info.code = code
    let request = NSMutableURLRequest(URL: NSURL(string: accessTokenRequestURL)!)
    request.HTTPMethod = "POST"
    var bodyData = accessTokenHTTPBody!
    request.HTTPBody = bodyData.dataUsingEncoding(NSUTF8StringEncoding);
    NSURLConnection.sendAsynchronousRequest(request,
        queue: NSOperationQueue.mainQueue(),
        completionHandler: {
            (_, data, _) in
            let dataString = NSString(data: data, encoding: NSUTF8StringEncoding)
            let jsonResult = NSJSONSerialization.JSONObjectWithData(data,
                options: NSJSONReadingOptions.MutableContainers,
                error: nil) as! NSDictionary
            Info.access_token = jsonResult["access_token"] as? String
            self.delegate?.didAuthrization(true)
    })
}

これでリクエストを送信すると、無事access_tokenが取得できます。

データの取得

データ取得に関しての注意点

データを取得するためのリクエストにはaccess_tokenを添付する必要がありますが、wunderlistはHTTPのヘッダーの中に情報を格納するように要求しています。 また、X-Access-TokenとX-Client-IDというフィールドを作成する必要があります。 なので、リクエストを作成する際に、以下のようにしてヘッダーを追加します。

今回はエンドポイントを受け取りNSMutableURLRequestにして返す関数を作成しました。 その中でヘッダーにclient_idとaccess_tokenの情報を追加しています。

private func getRequest(endpoint: String) -> NSMutableURLRequest? {
    if let url = NSURL(string: endpoint) {
        let request = NSMutableURLRequest(URL: url)
   request.addValue(Info.access_token, forHTTPHeaderField: "X-Access-Token")
        request.addValue(Info.client_id, forHTTPHeaderField: "X-Client-ID")
        return request
    }
    return nil
}

Listの取得

taskを取得するためには、まずlistを取得してlist_idを知る必要があります。

listを取得するためのエンドポイントはhttps://a.wunderlist.com/api/v1/listsです。 パラメータは特に必要ないので、とりあえずリクエストをおくります。

WunderlistのAPIに関するクラスにgetメソッドを作り、引数としてenumで宣言しておいた何が欲しいかの情報を渡します。 返ってきたjsonからidやtitleなどの情報を取得します。

サンプルの中では、listを取得した際にidを利用してtaskを取得するようにしています。

Wunderlist.sharedInstance.get(.List) {
    if let array = $0 {
        for listObj in array {
            let list = listObj as! NSDictionary
            let id = list["id"] as! NSNumber
            let title = list["title"] as! String
            println("list: id=\(id), title=\(title)")
            self.getTasks(id.stringValue)
            self.lists.append(list)
        }
    }
}

Taskの取得

listを取得した後には、list_idを利用してtaskを取得します。

エンドポイントはhttps://a.wunderlist.com/api/v1/tasksで、必要なパラメータはlist_idです。 また、パラメータにcompletedを追加すると完了済み/未完了のタスクを指定して取得することもできます。

func getTasks(listID: String) {
    Wunderlist.sharedInstance.get(.Task, parameters: ["list_id":listID]) {
        if let array = $0 {
            for taskObj in array {
                let task = taskObj as! NSDictionary
                let id = task["id"] as! NSNumber
                let title = task["title"] as! String
                let completed = task["completed"] as! NSNumber
                let due = task["due_date"] as? String
                println("    task: id=\(id), title=\(title), due=\(due), completed=\(completed.boolValue)")
                self.tasks.append(task)
            }
        }
    }
}

SwiftからTodoistのTodoを取得する

TodoistとTodoistAPIで、毎朝のタスク報告をするヾ(‘ω’)ノを見てSwiftでTodoist使って何かできないかなーと思い、とりあえずAPI使ってみました。

参考サイト Todoist API Documentation

事前準備

こちらにてClient IDとClient Secretを取得する

認証

今回はWebViewを使って、認証をおこなえるようにしました。

STEP1

まずhttps://todoist.com/oauth/authorize?client_id=<CLIENT_ID>&scope=<SCOPE>&state=<STATE>&redirect_uri=<REDIRECT_URI>にリクエストを送ります。

パラメータは4つあります。

  • client_id: 事前準備で取得したClient IDです
  • scope: 権限の強さを指定します
    • task:add, data:read, data:read_write, data:delete, project:delete
  • state: 好きな文字列。CSRFから守るためとありました。
  • redirect_uri: リダイレクト先 (Optional, DeveloperサイトからDefaultを設定することができる)

STEP2

リクエストを送信すると、https://todoist.com/Users/showLogin?<PARAMETERS> が返ってきます。これは以下のようなGoogleかemailでログインすることができるビューです。 このビューをwebViewに表示しユーザーがログインできるようにします。 f:id:rayc5:20150601055724p:plain

STEP3

ユーザーがログインしたら、以下の「同意する」のビューになります。 ここで同意するをクリックすると、STEP1で指定したリダイレクト先にリダイレクトされます。

f:id:rayc5:20150601055719p:plain

このときに、パラメーターとしてstateとcodeの2つが添付されています。 このcodeが次に必要になるので、URLからcodeを取得します。

URLはhttps://<リダイレクト先>?state=<STATE>&code=<CODE>という形をしているので、urlからパラメータ部分を抽出し、stateの文字数がわかっているのでそれをもとにcodeの部分だけ抜き出しました。 (もうちょっとスマートにやりたいんですけど何かうまい方法無いのかな。。)

func webView(webView: UIWebView, shouldStartLoadWithRequest request: NSURLRequest, navigationType: UIWebViewNavigationType) -> Bool {
        let url = request.URL!
        if url.host == "<リダイレクト先のホスト>" {
            let query = url.query!
            // stateの長さに依存する. 12+(stateの長さ). stateをstateに設定したので今回は17.
            let code = query.substringFromIndex(advance(query.startIndex, 17))
            
            // codeを利用してaccess_tokenを取得する処理
            // -- <略> --
        }
        return true
    }

STEP4

先ほど取得したcodeを利用してaccess_tokenを取得します。 https://todoist.com/oauth/access_token?client_id=<CLIENT_ID>&client_secret=<CLIENT_SECRET>&code=<CODE>にPOSTリクエストを送信します。

ここでのパラメータは3つです。

  • client_id: 事前準備で取得したClient ID
  • client_secret: 事前準備で取得したClient Secret
  • code: STEP3で取得したcode

これでaccess_tokenを取得できるので、大事に保存しましょう。

認証で引っかかった問題

STEP2でGoogleでログインしようとしたときにうまくリダイレクトしてくれませんでした。 メールならログインした後に以下の画面が表示されるのですが、Googleのときはローディングで一切進みません。

以下の画面を表示するために、Googleでログインが完了したタイミングでもう一度STEP1に戻ってリクエストを送信しました。 STEP1のリクエストですが、すでにログインしている場合はメールアドレス/パスワードを入力する画面ではなく、同意するかどうかの画面に飛びます。 なので、もう一度リクエストを送信して同意するかどうかの画面を表示するようにしました。

データ取得

タスクの取得

タスクのリストを取得するためにはhttps://todoist.com/API/v6/sync?token=<TOKEN>&seq_no=0&seq_no_global=0&resource_types=[\"items\"]にリクエストを送信します。

パラメータは以下の4つです。

  • token: アクセストークン
  • seq_no: Sequence number. 初めは0, 2回目以降は前回に受け取ったものを利用する.
  • seq_no_global: Global sequence number. 初めは0, 2回目以降は前回に受け取ったものを利用する.
  • resource_types: 取得したい情報を入力。リスト形式で複数渡すことが可能。
    • パラメータの種類は, projects, items, labels, notes, filters, reminders, locations, user, live_notifications, day_orders, allがある.

こんな感じで取得できました。

func getTasks() {
    let request = NSMutableURLRequest(URL: NSURL(string: getTasksURL)!)
    NSURLConnection.sendAsynchronousRequest(request,
        queue: NSOperationQueue.mainQueue(),
        completionHandler: {
            (res, data, error) in
            let dataString = NSString(data: data, encoding: NSUTF8StringEncoding)
            let jsonResult = NSJSONSerialization.JSONObjectWithData(data,
                options: NSJSONReadingOptions.MutableContainers,
                error: nil) as! NSDictionary
            
            let tasks = jsonResult["Items"] as! NSArray
            for task  in tasks {
                let task = task as! NSDictionary
                let id = task["id"] as! NSNumber
                let title = task["content"] as! String
                let date = task["due_date"] as! String
                println("id: \(id.stringValue), title: \(title), date: \(date)")
            }
    })
}
// 出力結果
id: 71504480, title: today's task 2, date: Mon 01 Jun 2015 04:59:59 +0000
id: 71504414, title: today's task 1, date: Mon 01 Jun 2015 04:59:59 +0000
id: 71504481, title: tomorrow's task 1, date: Tue 02 Jun 2015 04:59:59 +0000

まとめ

よくわからないところで引っかかり、ググってもイマイチ情報が出てこないので時間とられました。 Documentのサンプルがうまく動かないとかは止めて欲しいですね。

サンプルとして全体像を載せておきます。 適当すぎですが、とりあえずは動くはず。。

Todoist_API_sample

SwiftでGoogle Calendarの情報を取得してみた

SwiftからGoogleのCalendar APIを利用してカレンダー情報を取得してみました。 内容は認証の後、取得したカレンダーのデータをコンソールに出力するだけです。

参考サイト

Google Developer iOS Quickstart

事前準備

Google Developer iOS Quickstart のStep1に従ってClient IDを取得

CocoaPodでライブラリをインストール

Podfileに以下を追加し、pod install します。

pod 'Google-API-Client/Calendar', '~> 1.0'

Swiftから利用できるようにBridging-Headerに以下の情報を追加します。

#import "GTMOAuth2ViewControllerTouch.h"
#import "GTLCalendar.h"

カレンダーの情報を取得

import UIKit

class ViewController: UIViewController
{
    private var calendarService: GTLServiceCalendar? = nil
    
    private struct Info {
        static let KeychainItemName: String = "Google Calendar Quickstart"
        static let ClientID: String = "YOUR_CLIENT_ID_HERE"
        static let ClientSecret: String = "YOUR_CLIENT_SECRET_HERE"
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // 端末に保存されている情報を読み込む
        calendarService = GTLServiceCalendar.init()
        calendarService?.authorizer = GTMOAuth2ViewControllerTouch.authForGoogleFromKeychainForName(
            Info.KeychainItemName,
            clientID: Info.ClientID,
            clientSecret: Info.ClientSecret)
    }
    
    override func viewDidAppear(animated: Bool) {
        super.viewDidAppear(animated)
        
        // 認証がまだなら認証画面を出す. 認証済みならデータを取得
        if let canAuthorize = calendarService?.authorizer.canAuthorize {
            if !canAuthorize {
                self.presentViewController(createAuthController(), animated: true, completion: nil)
            } else {
                fetchEvents()
            }
        }
    }
    
    // 認証画面を作成して返す
    private func createAuthController() -> GTMOAuth2ViewControllerTouch{
        let authController = GTMOAuth2ViewControllerTouch(
            scope: kGTLAuthScopeCalendarReadonly,
            clientID: Info.ClientID,
            clientSecret: Info.ClientSecret,
            keychainItemName: Info.KeychainItemName,
            delegate: self,
            finishedSelector: "viewController:finishedWithAuth:errorOrNil:")
        return authController
    }
    
    // カレンダーのイベントを取得してコンソールに出力
    private func fetchEvents() {
        // クエリの設定.イベントは10個, 時間順にソート
        let query = GTLQueryCalendar.queryForEventsListWithCalendarId("primary") as! GTLQueryCalendar
        query.maxResults = 10
        query.timeMin = GTLDateTime(date: NSDate(), timeZone: NSTimeZone.localTimeZone())
        query.singleEvents = true
        query.orderBy = kGTLCalendarOrderByStartTime
        
        // クエリ実行. 完了後の処理はClosureで渡す
        calendarService?.executeQuery(query, completionHandler:{ (_, events, errorOrNil) -> Void in
            if let error = errorOrNil {
                self.showAlert("Error", message:"Unable to get calendar events.")
            } else {
                var result = ""
                if let items = events.items {
                    if items.count == 0 {
                        result = "No upcoming events found."
                    } else {
                        result = "Upcoming 10 events:\n"
                        for object in items {
                            if let event = object as? GTLCalendarEvent {
                                let start: GTLDateTime = event.start.dateTime ?? event.start.date
                                let startString = NSDateFormatter.localizedStringFromDate(
                                    start.date,
                                    dateStyle: NSDateFormatterStyle.ShortStyle,
                                    timeStyle: NSDateFormatterStyle.ShortStyle)
                                // イベントは開始時間とタイトルを出力
                                result += "\(startString) - \(event.summary)\n"
                            }
                        }
                    }
                } else {
                    result = "events.items() == nil\n"
                }
                // 結果の出力
                println(result)
            }
        })
    }
    
    // 認証後に呼ばれる関数.完了したら認証画面を閉じてイベントの取得.
    func viewController(viewController: GTMOAuth2ViewControllerTouch, finishedWithAuth authResult: GTMOAuth2Authentication, errorOrNil: NSError?) {
        if let error = errorOrNil {
            showAlert("Authentication Error", message: error.localizedDescription)
            calendarService?.authorizer = nil
        } else {
            calendarService?.authorizer = authResult;
            self.dismissViewControllerAnimated(true, completion: nil)
        }
    }
    
    // エラーを表示する
    private func showAlert(title: String, message: String) {
        let alert = UIAlertView(title: title, message: message, delegate: nil, cancelButtonTitle: "OK")
        alert.show()
    }
    
}

感想

SwiftからObjective-cのライブラリを利用するので、Optionalの扱いに気をつけないといけないですねー。

Xcodeはoption(alt)を押して変数をクリックすると型などの情報が表示されるので、それを使いながら確認して進めていきました。

SwiftyJson コードリーディングしてみた

SwiftyJsonをコードリーディングしてみました。

僕はobjective-cは1年近く触っていますが、swiftはまだ全然勉強していないという人です。 複雑な処理がないので初心者でも読みやすく、subscriptやSequenceTypeといった基本的なことがわかるのでオススメです。

SwiftyJsonとは

SwiftyJsonとはswiftjsonを使うときにはほぼデファクトとなっているライブラリです。 以下のような感じで簡単にjsonを使えるので便利です。

let json: JSON = JSON(data: dataFromNetworking)
let user: <String, JSON> = json["user"]. dictionaryValue
let name: String = json["user"]["name"].string

さて、コードリーディングをして気になった部分をメモしていきます。

変数の型のチェック方法

型のチェックはobject:AnyObjectのsetterの中で以下のようにas を利用して書かれていました。

あと、swiftのswitch文ではbreakが必要無いんですね。逆に続けて処理をしたい時はfallthroughと書くらしいです。 switch文の中では似たような方法でlet n where n < 10のように範囲指定したりもできるそうです。タプルも使えるみたいなので、swiftのswitch文の柔軟性は凄いですね。

switch newValue {
case let number as NSNumber:
    if number.isBool {
        _type = .Bool
    } else {
        _type = .Number
    }
case let string as NSString:
    _type = .String
case let null as NSNull:
    _type = .Null
case let array as [AnyObject]:
    _type = .Array
case let dictionary as [String : AnyObject]:
    _type = .Dictionary
default:
    // error handling
}

number.isBoolは以下のようにextensionを書いて実装していました。 swiftではtrue/falseを1/0で表現しているためBoolのtrueかNumberの1か判断できないのですが、 String.fromCString(trueNumber.objCType)を利用することで判断しています。

NSNumberはプリミティブ型のラッパークラスなのでbool, int, long, floatなど様々な値を代入することができ、objCTypeプロパティはその中のどれが代入されているのかを示すものです(多分)。playgroundで確認したところ、Boolならば"c"が返ってきました。

private let trueNumber = NSNumber(bool: true)
private let falseNumber = NSNumber(bool: false)
private let trueObjCType = String.fromCString(trueNumber.objCType)
private let falseObjCType = String.fromCString(falseNumber.objCType)

extension NSNumber: Swift.Comparable {
    var isBool:Bool {
        get {
            let objCType = String.fromCString(self.objCType)
            if (self.compare(trueNumber) == NSComparisonResult.OrderedSame &&  objCType == trueObjCType) ||  (self.compare(falseNumber) == NSComparisonResult.OrderedSame && objCType == falseObjCType){
                return true
            } else {
                return false
            }
        }
    }
}

subscript

subscriptとは[ ]で要素にアクセスするやつです。 SwiftyJsonでは以下のようにpathをarrayとして渡してアクセスすることができます。

let path = [9,"list","person","name"] 
let name = json[path]

パスのArrayの中は、ArrayのときにInt、DictionaryのときにStringを受け取ることができるので、IntとStringにSubscriptTypeプロトコルを追加し、[SubscriptType]を引数に受け取るように実装してありました。

Arrayの中に複数の型が入っているときは[AnyObject]とするのではなく、このように実装すると安全で良いですね。

public protocol SubscriptType {}
extension Int: SubscriptType {}
extension String: SubscriptType {}

extension JSON {
    public subscript(path: [SubscriptType]) -> JSON {
        get {
            if path.count == 0 {
                return JSON.nullJSON
            }
         
            var next = self
            for sub in path {
                next = next[sub:sub]
            }
            return next
        }
        set {
           // setter
        }
    }
}

subscriptの中ではエラーチェックの後、配列で渡されたパスをfor文を用いて一つづつ処理しています。 一つづつの処理はInt(Array)とString(Dictionary)のときで処理が変わるので振り分けるだけのメソッドを用意してあります。

その中の分岐でsub is Stringという構文が使われていました。 switch文の中では case let string as String、if文の中では sub is Stringとすることで型チェックができるみたいです。

private subscript(#sub: SubscriptType) -> JSON {
        get {
            if sub is String {
                return self[key:sub as! String]
            } else {
                return self[index:sub as! Int]
            }
        }
        set {
            // setter
        }
}

SequenceType

SwiftyJsonではjsonがArray/Dictionaryのときにfor..inを使ったループ処理ができます。

for (key: String, subJson: JSON) in json {
   //Do something you want
}

これを実現するためにはSequenceTypeプロトコルを実装する必要があります。 SequenceTypeプロトコルはgenerateメソッドを要求しています。

generateメソッドはGeneratorTypeプロトコルを実装しているGeneratorを返す必要があるのですが、今回はArrayとDictionaryを利用するのでGeneratorTypeについては気にする必要はありません。 もし自作クラスにSequenceTypeを適用するときにはGeneratorTypeも実装する必要があります。

SwiftyJsonではgenerateメソッドは以下のようになっています。 ArrayとDictionaryで処理を分けているだけですね。

GeneratorはGeneratorOfでClosureを使って実装しています。

public func generate() -> GeneratorOf <(String, JSON)> {
    switch self.type {
    case .Array:
        let array_ = object as! [AnyObject]
        var generate_ = array_.generate()
        var index_: Int = 0
        return GeneratorOf<(String, JSON)> {
            if let element_: AnyObject = generate_.next() {
                return ("\(index_++)", JSON(element_))
            } else {
                return nil
            }
        }
    case .Dictionary:
        let dictionary_ = object as! [String : AnyObject]
        var generate_ = dictionary_.generate()
        return GeneratorOf<(String, JSON)> {
            if let (key_: String, value_: AnyObject) = generate_.next() {
                return (key_, JSON(value_))
            } else {
                return nil
            }
        }
    default:
        return GeneratorOf<(String, JSON)> {
            return nil
        }
    }
}

Literal convertibles

Literal convertiblesとは以下のように異なる型でも代入できるようにすることです。 以下の例ではjsonJSON型として宣言されているのでStringは代入できないはずですが、JSONがStringLiteralConvertibleプロトコルを実装しているのでStringを代入することができます。

let json: JSON = "I'm a json"

StringLiteralConvertibleの他にはIntegerLiteralConvertibleやBooleanLiteralConvertibleなどもあります。 これらのプロトコルを実装することで異なる型の代入を可能にしています。

実装が必要なメソッドプロトコルによって異なりますが、基本的には内部で準備をして、initを呼ぶだけです。 SwiftyJsonではIntegerのときはinitを呼ぶだけですが、Dictionaryのときは引数をdictionaryに格納してからinitを読んでいることがわかります。 これは引数をArrayで受け取るため、Dictionaryに変換しています。

extension JSON: Swift.IntegerLiteralConvertible {
    public init(integerLiteral value: IntegerLiteralType) {
        self.init(value)
    }
}

extension JSON: Swift.DictionaryLiteralConvertible {
    public init(dictionaryLiteral elements: (String, AnyObject)...) {
        var dictionary_ = [String : AnyObject]()
        for (key_, value) in elements {
            dictionary_[key_] = value
        }
        self.init(dictionary_)
    }
}