iOS で Evernote API を使う(後編)

iOS で Evernote API を使う(前編) の続きです。今回はEvernoteからデータ(ノート)を取得し表示してみます。

http://www.evernote.com/about/media/img/logos/evernote_logo_4c-lrg.gif

Evernoteデータ形式

Evernoteのデータは Evernote Markup Language (ENML) と名づけられたXMLファイルです。しかし下の例から解るようにほぼHTMLです。

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE en-note SYSTEM "http://xml.evernote.com/pub/enml2.dtd">
<en-note style="word-wrap: break-word; -webkit-nbsp-mode: space; -webkit-line-break: after-white-space;">
<div style="padding-top: 0px; padding-right: 0px; padding-bottom: 0px; ・・・">
 ・・・
<h2 style="padding-top: 0px; padding-right: 0px; ・・・">問題解決型の教育</h2>
<p style="・・・ color: rgb(85, 85, 85); line-height: 20px;">EY-Officeの教育は知識の取得を目的とした学校的な教育ではなく、 実務で必要とされる技術を、
<br style="・・・ color: rgb(85, 85, 85); line-height: 20px;"/>
お客様のニーズや開発メンバーのスキルに応じ柔軟な教育プランを組み立てご提案させて頂く
<strong style="padding-top: 0px; padding-right: 0px; ・・・ ">「問題解決型の教育」</strong>&#160;です。</p>
 ・・・
<en-media title="EY-Officeの教育" alt="EY-Officeの教育" style="padding-top: 0px; ・・・"
   type="image/jpeg" hash="819b7511534b5ecb92ef4a6bc8650350"/>
  ・・・
</en-note>

HTMLとの違いは、

  • 開始タグ、終了タグ
  • 使われるタグは HTMLの一部
  • スタイル要素は全てstyle属性内に展開されている
  • 画像など(リーソースと呼ばれています)は独自のen-mediaタグが使われている、このタグの属性は
    • type: mineタイプ
    • hash: リソースのファイル名(Hash値)

なので、簡単に HTMLに変換できます。詳細は Evernote API Overview に書かれています。

通常のEvernoteデータへのアクセス

前編 で取得したキーではSandboxのデータしかアクセスできませんが、本物のデータをアクセスしたい場合は Evernote API Key Activation ページで申請することでキーがアクティベートされます。アクティベートはたぶん人手で行われている様で、USのビジネス時間になるとメールで返事が返ってきます。

テストアプリの改造

前編 で作ったアプリで表示されるノートの一覧をクリックすると、そのノートが別のページに表示されるようにします。ソースコード GitHub で公開しています。

→ クリック →


また、複数のページから Evernote APIをアクセスする必要があるので、Evernoteを扱うシングルトンのモデルを作りEvernoteとのやり取りはまとめます。

Evernoteモデル

Evernoteモデルの仕様(.h)は以下のようにしました
@interface Evernote : NSObject {
  @private
    NSString *authToken;
    EDAMNoteStoreClient *noteStore;
}
+ (Evernote *)sharedInstance;   // シングルトンインスタンスの取得
- (BOOL)authentication;  // 認証を実行
- (NSArray *)listNotebooks;  // ノートブックの一覧を取得
- (NSArray *)listNotesInNotebook:(NSString *)notebookName; // 指定されたノートブック内の全ノートの一覧を取得
- (NSURL *)getHtmlNoteByGuid:(NSString *)guid;  //指定されたID(guid)のノートデータを取得し、 HTMLに変換
@end
認証、ノートブックの一覧を取得の部分

前編作った認証、ノートブックの一覧の部分は以下のようなメソッドにしてみました。

|#import "Evernote.h"

#define USER_STORE_URL  @"https://www.evernote.com/edam/user"  //本当のデータをアクセスします
#define NOTE_STORE_URL  @"http://www.evernote.com/edam/note/" //本当のデータをアクセスします

#define USERNAM         @"XXXXXX"  //自分のログイン、 APIキーを入れて下さい
#define PASSWORD        @"XXXXXX"
#define CONSUMER_KEY    @"XXXXXX"
#define CONSUMER_SECRET @"XXXXXX"

#define RAISE_EXCEPTION_IF_ERROR(error) if (error) [NSException raise:@"Error" format:[error description]]

@interface Evernote ()
@property (nonatomic, retain) EDAMNoteStoreClient *noteStore;
@property (nonatomic, retain) NSString *authToken;
- (void)relaceMutableString:(NSMutableString *)s pattern:(NSString *)p replace:(NSString *)r;
- (NSString *)hexStringFromData:(NSData *)data;
@end

static Evernote *sharedInstanceDelegate = nil;

@implementation Evernote
@synthesize noteStore;
@synthesize authToken;

- (BOOL)authentication {
    self.authToken = nil;
    self.noteStore = nil;

    THTTPClient *userStoreHTTPClient = [[[THTTPClient alloc] initWithURL:[NSURL URLWithString:USER_STORE_URL]] autorelease];
    TBinaryProtocol *userStoreProtocol = [[[TBinaryProtocol alloc] initWithTransport:userStoreHTTPClient] autorelease];
    EDAMUserStoreClient *userStore = [[[EDAMUserStoreClient alloc] initWithProtocol:userStoreProtocol] autorelease];
	
    BOOL versionOK = [userStore checkVersion:@"EDMATest" :[EDAMUserStoreConstants EDAM_VERSION_MAJOR] :[EDAMUserStoreConstants EDAM_VERSION_MINOR]];
    if (!versionOK) {
        NSLog(@"checkVersion error");
        return NO;
    }
    @try {
        EDAMAuthenticationResult *authResult = [userStore authenticate:USERNAM :PASSWORD :CONSUMER_KEY :CONSUMER_SECRET];
        EDAMUser *user = [authResult user];
        self.authToken = [authResult authenticationToken];
        NSURL *noteStoreURL = [NSURL URLWithString:[NOTE_STORE_URL stringByAppendingString:[user shardId]]];

        THTTPClient *noteStoreHTTPClient = [[[THTTPClient alloc] initWithURL:noteStoreURL] autorelease];
        TBinaryProtocol *noteStoreProtocol = [[[TBinaryProtocol alloc] initWithTransport:noteStoreHTTPClient] autorelease];
        self.noteStore = [[[EDAMNoteStoreClient alloc] initWithProtocol:noteStoreProtocol] autorelease];
        return YES;
    }
    @catch (NSException *e) {
        NSLog(@"Exception %@  %s", [e description], __FUNCTION__);
        return NO;
    }
}

- (NSArray *)listNotebooks {
    @try {
        return [noteStore listNotebooks:authToken];
    }
    @catch (NSException * e) {
        NSLog(@"Exception %@  %s", [e description], __FUNCTION__);
        return nil;
    }
}
ノート一覧取得

今回は、指定されたノートブック内の全ノートの一覧を取得するメソッドを作りました。
また、ノートの一覧(findNotes)は最大取得件数(findNotes) に大きな値を設定しても適当な件数(50件)しか戻しませんので、全ノートが取得できてない場合は続きを取得するようにしてみました。
このメソッドで戻るのはノートデータ(EDAMNote)の配列ですが、findNotesの戻すEDAMNoteにはノートのコンテンツは含まれていませんので、表示する際には改めてコンテンツを取得する必要があります。

- (NSArray *)listNotesInNotebook:(NSString *)notebookName {
    @try {
        NSString *words = notebookName ? [@"notebook:" stringByAppendingString:notebookName] : nil;
        EDAMNoteFilter *allFileter = [[[EDAMNoteFilter alloc] initWithOrder:0 ascending:NO words:words notebookGuid:nil tagGuids:nil
                                                                   timeZone:nil inactive:NO] autorelease];
        NSMutableArray *list = [NSMutableArray array];
        EDAMNoteList *subList;
        NSArray *notes;
        int offset = 0;
        do {
            subList = [noteStore findNotes:authToken :allFileter :offset :1000];
            notes = [subList notes];
            // NSLog(@"notes count %d, %d, %d", [notes count], [subList totalNotes], [subList startIndex]);
            [list addObjectsFromArray:notes];
            offset += [notes count];
        } while (offset < [subList totalNotes]);
        
        // NSLog(@"list count %d", [list count]);
        return list;
    }
    @catch (NSException * e) {
        NSLog(@"Exception %@  %s", [e description], __FUNCTION__);
        return nil;
    }
}
ノートデータを取得しHTMLに変換するメソッド

このメソッドではテンポラリーディレクトリーに1つディレクトリーを作り、そこにコンテンツのhtml、リソースの画像データを格納しています。戻り値はそのディレクトリーのURLです。これを使って UIWebViewでコンテンツを表示できます。

  • ノートコンテンツの取得はgetNote:メソッドを使います。引数の指定でノート内に含まれている画像等のリソースを一度に取得する事もできますが、今回はリソースは別にしました。
  • 上で説明したように、取得された XMLの一部を書き換える事で HTMLに出来ます。ただし、en-mediaタグを画像に変換する部分はEvernote側の都合で属性の順番が変わると動作しなくなるので注意して下さい :-)
  • 画像は ハッシュ.image.png のようなファイル名になります。
  • リーソースはEDAMNoteのresourcesの情報を使いgetResource:メソッドで取得しています。
  • コンテンツ内に <div ... /> というタグがある場合、表示がおかしくなるので <div ...></div>に変換しています。(DOCTYPE とかでなんとかならないのかな?)
- (NSURL *)getHtmlNoteByGuid:(NSString *)guid {
    EDAMNote *note;
    @try {
        note = [noteStore getNote:authToken :guid :YES :NO :NO :NO];
        NSLog(@"- note %@ : %@  : %d %d", [note guid], [note title], [[note content] length], [[note resources] count]); 

        NSMutableString *html = [NSMutableString stringWithString:[note content]];
        // NSLog(@"--\n%@\n", html);
        [self relaceMutableString:html pattern:@"^.*<en-note.*?>" 
                          replace:@"<html><head><title>note</title><meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" /></style></head><body>"];
        [self relaceMutableString:html pattern:@"<\\/en-note>" replace:@"</body></html>"];
        [self relaceMutableString:html pattern:@"<en-media.*?style=\"(.*?)\".*?type=\"(\\w+?)/(\\w+?)\".*?hash=\"(\\w+?)\"/>"
                          replace:@"<img src=\"$4.$2.$3\" style=\"$1\" />"];
        [self relaceMutableString:html pattern:@"<en-media.*?style=\"(.*?)\".*?hash=\"(\\w+?)\".*?type=\"(\\w+?)/(\\w+?)\">.*?</en-media>"
                          replace:@"<img src=\"$2.$3.$4\" style=\"$1\" />"];
        [self relaceMutableString:html pattern:@"<div ([^>]*?)/>" replace:@"<div $1></div>"];
        //NSLog(@"--  html --\n%@\n", html);
        
        NSFileManager *fileManager = [NSFileManager defaultManager];
        NSError *error = 0;
        NSString *contentsPath = [NSTemporaryDirectory() stringByAppendingString:@"contents"];
        if ([fileManager fileExistsAtPath:contentsPath]) {
            [fileManager removeItemAtPath:contentsPath error:&error];
            RAISE_EXCEPTION_IF_ERROR(error);
        }
        [fileManager createDirectoryAtPath:contentsPath withIntermediateDirectories:NO attributes:nil error:&error];
        RAISE_EXCEPTION_IF_ERROR(error);
        
        NSString *htmlPath = [contentsPath stringByAppendingString:@"/index.html"];
        [html writeToFile:htmlPath atomically:NO encoding:NSUTF8StringEncoding error:&error];
        RAISE_EXCEPTION_IF_ERROR(error);
        NSLog(@"write html %@", htmlPath);
        
        for (EDAMResource *resourceInfo in [note resources]) {
            EDAMResource *resource = [noteStore getResource:authToken :resourceInfo.guid :YES :NO :NO :NO];
            NSString *hash = [self hexStringFromData:[[resource data] bodyHash]];
            NSString *ext = [resource.mime stringByReplacingOccurrencesOfString:@"/" withString:@"."];
            NSString *path = [contentsPath stringByAppendingFormat:@"/%@.%@", hash, ext];
            [[[resource data] body] writeToFile:path options:0 error:&error];
            RAISE_EXCEPTION_IF_ERROR(error);
            NSLog(@"write res  %@", path);
        }

        return [NSURL fileURLWithPath:htmlPath isDirectory:NO];
    }
    @catch (NSException *e) {
        NSLog(@"Exception %@  %s", [e description], __FUNCTION__);
        return nil;
    }
}
- (void)relaceMutableString:(NSMutableString *)s pattern:(NSString *)p replace:(NSString *)r {
    NSError *error = NULL;
    NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:p
                                                                           options:NSRegularExpressionDotMatchesLineSeparators
                                                                             error:&error];
    [regex replaceMatchesInString:s options:0 range:NSMakeRange(0, [s length]) withTemplate:r];
}

- (NSString *)hexStringFromData:(NSData *)data {
    NSMutableString *hex = [NSMutableString string];
    
    unsigned char *buff = (unsigned char *)[data bytes];
    for (int i = 0; i < [data length]; i++) {
        [hex appendFormat:@"%02x", buff[i]];
    }
    
    return  hex;
}

その他のコード

モデルのその他の部分、ビュー・コントローラ等のコードは、ソースコードGitHub https://github.com/yuumi3/EvernoteTest で公開していますので、それを見て下さい。