2010年4月30日金曜日

データストアのメンテナンスモードに対応する

Google App Engineのデータストアは定期的にメンテナンスモードに入ります。 メンテナンス中はデータストアが読み取り専用になって一切の書き込みが禁止されます。

Python版にはCapabilityServiceというものが用意されていてこれらを調べるのは簡単ですが、Java版にはまだ用意されていないようです。

Java版で(無理せず)これをテストするには、次のようなサーブレットフィルタを用意してやります。

package com.example;

import java.io.IOException;
import java.util.concurrent.Future;

import javax.servlet.*;

import com.google.apphosting.api.ApiProxy;
import com.google.apphosting.api.ApiProxy.*;

public class MaintenanceFilter implements Filter {

    @Override
    public void init(FilterConfig conf) throws ServletException {
        return;
    }

    @Override
    public void destroy() {
        return;
    }

    @Override
    public void doFilter(
            ServletRequest req,
            ServletResponse res,
            FilterChain chain) throws IOException, ServletException {
        @SuppressWarnings("unchecked")
        Delegate<Environment> delegate = ApiProxy.getDelegate();
        try {
            ApiProxy.setDelegate(new MaintenanceDelegate(delegate));
            chain.doFilter(req, res);
        }
        finally {
            ApiProxy.setDelegate(delegate);
        }
    }

    private static class MaintenanceDelegate implements Delegate<Environment> {

        private final Delegate<Environment> delegate;

        MaintenanceDelegate(Delegate<Environment> delegate) {
            this.delegate = delegate;
        }

        @Override
        public void log(Environment env, LogRecord record) {
            this.delegate.log(env, record);
        }

        @Override
        public Future<byte[]> makeAsyncCall(
                Environment env,
                String service,
                String method,
                byte[] bytes,
                ApiConfig config) {
            return this.delegate.makeAsyncCall(env, service, method, bytes, config);
        }

        @Override
        public byte[] makeSyncCall(
                Environment env,
                String service,
                String method,
                byte[] bytes) throws ApiProxyException {
            if (service.equals("datastore_v3")) {
                if (method.equals("Put") || method.equals("Delete")) {
                    throw new CapabilityDisabledException(service, method);
                }
            }
            return this.delegate.makeSyncCall(env, service, method, bytes);
        }
    }
}

これをweb.xmlに登録します。このとき、「/_ah/」以下をフィルタするといろいろ不具合が発生するので、必要最小限にしましょう。

<filter>
    <filter-name>MaintenanceFilter</filter-name>
    <filter-class>com.gluegent.goose.soc.front.util.MaintenanceFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>MaintenanceFilter</filter-name>
    <url-pattern>/gadget/*</url-pattern>
    <dispatcher>REQUEST</dispatcher>
</filter-mapping>

このフィルタを経由した状態でデータストアに書き込みを行うと、CapabilityDisabledExceptionがスローされるようになります。 例外をキャッチしてメンテナンス中の処理を書いておきましょう。

try {
    // データストアへの書き込み
}
catch (CapabilityDisabledException e) {
    // メンテナンス中の処理
}

メンテナンスモードのときの動作がエミュレーションされるので、この状態で様々なテストを実施します。 なお、このフィルタを残したままサービスインするとひどいことになります。

追記

ご指摘いただきました。 メンテナンスモードのときはDatastoreService.allocateIds()やMemcacheなども動かなくなるので、makeSyncCallメソッドの中でもう少しトラップが必要です。 具体的にはこんな感じ:

@Override
public byte[] makeSyncCall(
        Environment env,
        String service,
        String method,
        byte[] bytes) throws ApiProxyException {
    if (service.equals("datastore_v3")) {
        if (method.equals("Put") ||
                method.equals("Delete") ||
                method.equals("AllocateIds")) {
            throw new CapabilityDisabledException(service, method);
        }
    }
    else if (service.equals("memcache")) {
        throw new CapabilityDisabledException(service, method);
    }
    return this.delegate.makeSyncCall(env, service, method, bytes);
}

2010年4月27日火曜日

カメラ、フォトライブラリを表示する

カメラ、フォトライブラリを表示、利用するにはUIImagePickerControllerを利用する。UIImagePickerControllerSourceTypeというenumがあるのでこれを利用してカメラ、フォトライブラリなどのいずれを利用するか決める。

カメラを表示するソース。delegeteに自身を渡す場合はUIImagePickerControllerDelegate, UINavigationControllerDelegateの両方を実装していなければならない。

// 有効かどうかを確認する
if (![UIImagePickerController 
      isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]) {  
    return;
}
    
UIImagePickerController *cameraController = 
        [[[UIImagePickerController alloc] init] autorelease];
cameraController.sourceType = UIImagePickerControllerSourceTypeCamera;
cameraController.allowsImageEditing = NO;
cameraController.delegate = self;
[self presentModalViewController:cameraController animated:YES];

2010年4月26日月曜日

App Engine SDK 1.3.3.1 リリース (?)

先日にGAEのSDK 1.3.3がリリースされましたが、今回はJava版のバグフィックスのようです。(Python版は出ていません)

  • http://code.google.com/p/googleappengine/issues/detail?id=3138

SDK 1.3.3のServlet Filterでワイルドカードを指定した際にうまく動作しなかったバグを修正したとのことです。

ダウンロードはプロジェクトのDownloadsのページから。

ただ、日本時間で2010/04/26 10:31の時点では、上記のIssueが完了していなかったり、リリースノートのページにリリースが出ていなかったりでどういう扱いなのかちょっと不明です。

追加情報があれば改めてお知らせします。

2010年4月23日金曜日

UITableViewCellの現在の位置を取りたい

UITableViewCellの現在表示されている位置を取得しよう、というんで素直に

CGRect rect = cell.frame;

と取ってみたくなりますが、スクロールが行われている場合はこれでは取れないです。このrectの持っている座標は、スクロールが行われてない場合の位置、つまり画面の外を指しています。

なので、どれだけUITableViewがスクロールされているかを取って引き算してやればいい、と。UITableViewにcontentOffsetなんてプロパティがあります。

CGPoint offset =  tableView.contentOffset;
rect.origin.x = rect.origin.x - offset.x;
rect.origin.y = rect.origin.y - offset.y;

めでたきかなこれで取れました。

autoreleaseのタイミング

前回のわたくしのNSIndexPathについてのポストで、autoreleaseだとメモリ圧迫しちゃうねぇなどと書きましたが。

Xcodeで自動で生成されるソースを見ると、main.mに

int main(int argc, char *argv[])
{

  NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
  int retVal = UIApplicationMain(argc, argv, nil, nil);
  [pool release];
  return retVal;
}

などとありまして、ああautoreleaseにした奴はアプリケーションが終了するまでプールされっぱなしなのだな、これは気をつけなきゃいけないなと思っていたのですが。

敢えてautoreleaseにしてたオブジェクトが予期せぬタイミングで初期化されていてエラーで落ちるという現象が起きてびっくり。調べてみたら全く同様の事を記事にしてらっしゃる方がいました。

http://insideflag.blogspot.com/2009/08/autorelease.html

どうやらUIKitでは、イベント発生でプールが作られて終了でreleaseされるようです・・・。なんてこった。いや楽になってよかった。

App EngineのBlobstoreにアップロードしたデータを解析する

Blobstoreのデータをアプリケーションで利用するに引き続き、どんな感じでGAEのBlobstoreにアップロードしたデータを解析しているか紹介。

とりあえず、下記のような形でPOSTとPUTをハンドルするサーブレットを作ってます。 POSTはBlobstoreにアップロードした後のリダイレクト先で、PUTはさらにそれを解析する部分です。

...
public class BlobAnalyzeServlet extends HttpServlet {

    private static final long serialVersionUID = 1L;

    private static final String K_BLOB_KEY = "k";

    private transient BlobstoreService blobstore;

    @Override
    public void init() throws ServletException {
        readResolve();
    }

    private Object readResolve() {
        blobstore = BlobstoreServiceFactory.getBlobstoreService();
        return this;
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp)
            throws IOException {
        Map<String, BlobKey> blobs = blobstore.getUploadedBlobs(req);
        if (blobs.size() != 1) { // この例では同時に1つだけアップロード
            resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
            return;
        }
        BlobKey key = blobs.values().iterator().next();

        // TaskQueueを利用して裏側でdoPutを実行
        QueueFactory.getDefaultQueue().add(
            TaskOptions.Builder
                .url(req.getServletPath())
                .method(TaskOptions.Method.PUT)
                .param(K_BLOB_KEY, key.getKeyString()));

        resp.sendRedirect("/<成功した際のリダイレクト先>");
    }

    @Override
    protected void doPut(HttpServletRequest req, HttpServletResponse resp)
            throws IOException {
        String keyString = req.getParameter(K_BLOB_KEY);
        BlobKey blobKey = new BlobKey(keyString);
        InputStream input = new BlobInputStream(blobKey, 512 * 1024);

        // 以降、blobの内容を解析
    }
}

なお、doPutの中で使っているBlobInputStreamは、前回紹介したプログラムと同じです。第2引数の「512 * 1024」は一度にフェッチするサイズで、最大の1MBの半分を指定しています。 アップロードされたファイルの内容を直に取れるので、ファイルの内容を解析したりできます。

TaskQueueを利用している関係で、アップロードしてから解析が終わるまでに少し遅延がありますが、解析を別プロセスで行うためリクエストを速くさばけるという利点があります。

Blobstore自体の使い方は「AppEngine1.3.0 Blobstore API入門」辺りを参考にしてください。

2010年4月22日木曜日

Google App Engine SDK 1.3.3 リリース

GAEのSDK 1.3.3がリリースされました。リリースノートはこちら

プレリリースの内容がすべて盛り込まれたわけでもない感じ(データストアのID自動採番時に範囲を指定できるメソッドが入っていない)で、いくつかのバグフィックスや細かい修正が主です。いくつかダイジェストで紹介します。

  • アプリケーションIDとバージョンを確認するシステムプロパティの追加

System.getProperty()メソッドでデプロイした際のアプリケーションIDとバージョンをアプリケーションから取得できるようになりました。

プロパティ名を覚えるのが大変なので、SystemPropertyというクラスを使うといいと思います。

import com.google.appengine.api.utils.SystemProperty;
...

String id = SystemProperty.applicationId.get();
String version = SystemProperty.applicationVersion.get();
  • 30秒制限の前に確実にキャッチできるDeadlineExceededExceptionがスローされるようになった

これがスローされなかったことは今まで見たことがないですが、確実にスローされるようにした、というところでしょうか。

  • QuotaService.getCpuTimeInMegaCycles()の修正

これまでは「InMegaCycles」と言いながらメガじゃないCPUサイクル数を返していたようです。

  • トランザクション時に一部の例外を識別できるようになった (?)

CommittedButStillApplyingExceptionという例外が増えています。

App Engineのデータストアでトランザクションを利用してエンティティを書きこむと、Life of a Datastore Writeにある通りCommit, Applyという2つのフェーズが順に行われます。このうち、Applyでエラーが起こった場合にはエンティティは書き込まれているもののインデックスが不整合の状態になります。

これまではCommitで失敗してもApplyで失敗しても同じエラーが返されていましたが、この例外がスローされた場合はエンティティの書き込み自体は成功していることが分かります。インデックスを反映させる(Apply)には、そのエンティティをトランザクション内で読みに行くだけで自動的にロールフォワードという処理が行われます。

この例外がスローされる状況をまだ再現するほど実験していないので、追加情報があれば後日お知らせします。

Java版の更新はだいたい以上です。なお、Python版のSDKは、開発サーバ上のデータストアにバックエンドとしてSQLiteを指定できるようになったようです。

2010年4月21日水曜日

ModalViewの表示時のアニメーションを変更する

UIViewControllerのプロパティmodalTransitionStyleを変更することで可能。

以下の3種類用意されている。

  • UIModalTransitionStyleCoverVertical
    • 下から上にせり出してくる。デフォルトはこれ。
  • UIModalTransitionStyleCrossDissolve
    • 画面がフェードアウト、フェードインする
  • UIModalTransitionStyleFlipHorizontal
    • 水平方向に画面がくるっと回る(フリップする)

デフォルトのアニメーションを使いつつ、キーボードを表示した状態で画面を開くと、キーボードがあたかも画面に組み込まれてるパーツのように見せることが可能ということも分かった。キーボードを最初から表示させる為には次のようにすればいい。

@implementation ModalViewController
@synthesize textView;
-(void) viewDidLoad {
    // 最初のresponderに設定することで自動的にキーボードが表示される。
    [textView becomeFirstResponder];
    [super viewDidLoad];
}
@end

ModalViewの表示に関しては先程のエントリを参照。

自前でNSIndexPathを生成する

UITableViewで、現在の行の次の行や前の行を参照したい場合、NSIndexPathを自分で生成しないといけないのだろうなと思いつくわけですが、さてどうしたもんか。

調べると、こうすればよいという情報がそこここにあります。

NSIndexPath newPath = [NSIndexPath indexPathForRow:3 inSection:1];

この場合、2番目のセクションの4行目を表すNSIndexPath が出来ます。出来ますが。

indexなどと始まる名前のクラスメソッドの常で、これはファクトリクラスメソッドで、生成するインスタンスに対して内部でautoreleaseしてます。イベントなどでなんども発生するようなメソッド内で書くと、あっという間にメモリを食ってしまいます。なのでちゃんと自分でオーナーシップを握るようにしないといけません。

そもそもNSIndexPathって何だろうって話なのですが、一番判りやすい説明は「アウトライン上の位置情報」でした。

  • 1章
    • 1-1節
    • 1-2節
      • 1-2-3項
    • 1-3節
  • 2章
    • 2-1節 #いまここ
    • 2-2節

以下略

このいまここ、がNSIndexPath。階層は任意で決められます。UITableViewのDataSourceやDelegateのメソッドで扱ってるものは、UITableView用にカテゴリで拡張したもので、階層は2階層で固定、それぞれを表すsectionとrowというプロパティが追加されています。

なので自前で作るときはこうなります。

NSUInteger newIndex[] = {1, 3}; // 2番目のセクションの4行目
NSIndexPath newPath = [NSIndexPath alloc] initWithIndexes:newIndex length:2];
//ああだこうだの処理
[newPath release];

length: が階層ですね。これで自分でお掃除も出来ます。どうせならUITableView用に特化したイニシャライザも作ってくれればよかったのに。上の処理をラップすりゃ出来るんだし。

ModalViewの表示

今いる画面で一時的に表示したい(イメージ的には遷移を伴わない画面の表示)時に使う方法。

表示する時

// ModalViewControllerはUIViewControllerのサブクラス
ModalViewController *modalViewController = [[ModalViewController alloc]
                       initWithNibName:@"ModalView" bundle:nil];
[self presentModalViewController:modalViewController animated:YES];
[modalViewController release];

閉じる時

// ModalViewController側に書く
[self dismissModalViewControllerAnimated:YES];

自分のアプリをリンクで起動させる方法

iPhoneではサファリ(おそらくその他のブラウザからでも)から電話、SMS、Youtubeなどに対するリンクを自動的に判別し、 個々のアプリを開いてくれるという機能がありますが、せっかくのこの機能自分のアプリでも使いたい!と思って調べてみました。

結果以下手順で、サファリからリンクを入力して起動することが出来ました。

まず、info.plistに以下を追記。ただし、これはXCodeからいじるとkeyの名前が若干違って見えるので注意です。

<key>CFBundleURLTypes</key>
<array>
  <dict>
    <key>CFBundleURLName</key>
    <string>com.gluegent.showAlert</string>
    <key>CFBundleURLSchemes</key>
    <array>
        <string>launchtest</string>
    </array>
  </dict>
</array>

ApplicationDelegateに以下のメソッドを追記。

-(BOOL) application:(UIApplication *)application handleOpenURL:(NSURL *)url {
    // どこからでも呼ばれる可能性があるので
    // スキーマ名、パスなどきちんと確認する必要がある。
    if ([[url scheme] isEqualToString:@"launchtest"]) {
        UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"LaunchTest"
                              message:@"Hey, Success!!"
                              delegate:nil
                              cancelButtonTitle:nil
                              otherButtonTitles:@"Yappie!!", nil];
        [alert show];
        [alert release];
        return YES;
    }
    return NO;
}

最後にアプリをシミュレータで起動したあとで、サファリからURL入力欄にlaunchtest:com.gluegent.showAlertと入力するとアラートが表示されます。この情報は意外とたどり着けなかったんですが、普通に公式のTodoアプリのチュートリアルの中でも解説されていました。(リンクを失念。。。)

ただし、このリンクによる起動は注意しないと重大なセキュリテリィホールを作ってしまう危険性もあります。 詳しくはApple公式のScureCodingGuideを参考にしてみてください。

Blogger APIを利用してエントリを投稿する

Google Bloggerにエントリを投稿するツールを作っていました。この辺からGData関連のライブラリをダウンロードして使います。正式なドキュメントはBlogger APIにあります。

まず、投稿するエントリを作成する部分。

String CATEGORY_SCHEME = "http://www.blogger.com/atom/ns#";
String title = ...; // 投稿タイトル
String content = ...; // 内容 (HTML)
List<String> tags = ...; // タグ

Entry entry = new Entry();
entry.setTitle(new PlainTextConstruct(title));
entry.setContent(new HtmlTextConstruct(content));
for (String tag : tags) {
    entry.getCategories().add(new Category(CATEGORY_SCHEME, tag));
}

AuthSubで認証してエントリを投稿する部分。他にも認証形式はいくつかありますが、身内で使うツールだったらこれくらいでよいかと思います。

// 投稿画面のURLにblogID=... と書いてある数字
String blogId = ...;
// 投稿ツールのID
String toolId = "<company>-<tool>-<version>";
String user = ...; // ユーザ名
String password = ...; // パスワード

BloggerService service = new BloggerService(toolId);
service.useSsl();
service.setUserCredentials(
    user,
    password,
    ClientLoginAccountType.GOOGLE);
URL postUrl = new URL("http://www.blogger.com/feeds/" +
        blogId +
        "/posts/default");
Entry post = service.insert(postUrl, entry);

// 投降したエントリのURL
String result = post.getHtmlLink().getHref();

一点はまったのが、setUserCredentialsで"ClientLoginAccountType.GOOGLE"を指定する部分でした。 Google Appsと同じメールアドレスでGoogleアカウントを持っていると、通常では認証がGoogle Appsの方で行われてしまいます。 これを指定するとGoogle Appsでの認証を行わないので、普通に運用しているbloggerだったらこれを指定しておく方が無難です。

2010年4月16日金曜日

ネストしてるNSDictionaryをドット参照で取得する

NSDictionaryがネストされているケースなどの場合いちいち取得してキャストしてといったようなコードを書かないといけないのかと思ってましたが一発で抜けるメソッドがありました。これは便利!

NSDictionary* nested = ...;
// わざわざこう書かなくても
NSString toughName = [(NSDictionary*)[nested objectForKey:@"user"] objectForKey:@"name"];
// こう書ける
NSString easyName = [nested valueForKeyPath:@"user.name"];

2010年4月9日金曜日

Blobstoreのデータをアプリケーションで利用する

Google App Engine SDK 1.3.2からはBlobstoreService APIにfetchDataというメソッドが追加されています。 これまではBlobstoreにデータをアップロードしても、アプリケーションの中から中身を見ることができなかったのですが、これを使うとそれができるようになる様子です。

ただしこのメソッド、1回に1MBずつしか転送できなかったりして面倒なので、InputStreamでラップしてみました。軽く実験したところ10MBのファイルを1秒程度で読み出せたりするなど、妙に優秀なので何か間違っているんじゃないかと不安になってます。

以下、ラップしたプログラムです。+expandとかで全部見えると思います。

package com.example;

import java.io.IOException;
import java.io.InputStream;
import java.text.MessageFormat;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.appengine.api.blobstore.BlobInfo;
import com.google.appengine.api.blobstore.BlobInfoFactory;
import com.google.appengine.api.blobstore.BlobKey;
import com.google.appengine.api.blobstore.BlobstoreService;
import com.google.appengine.api.blobstore.BlobstoreServiceFactory;

/**
* Blobの中身を読み出すストリーム。
*/
public class BlobInputStream extends InputStream {

private static final Logger LOG = LoggerFactory.getLogger(
    BlobInputStream.class);

private transient BlobstoreService service;

private final BlobInfo info;

private final int chunkSize;

private long positionInBlob;

private transient int positionInChunk;

private transient byte[] chunk;

private boolean closed;

/**
 * インスタンスを生成する。
 * @param key 読みだす対象のBlobへのキー
 * @param chunkSize 分割して読みだす個々のチャンクのバイト数
 * @throws IOException Blobが存在しない場合や、Blobのメタ情報取得に失敗した場合
 * @throws IllegalArgumentException 引数に{@code null}が含まれる場合、
 *     または{@code chunkSize}に0以下の値が指定された場合
 */
public BlobInputStream(BlobKey key, int chunkSize) throws IOException {
    if (key == null) {
        throw new IllegalArgumentException("key is null"); //$NON-NLS-1$
    }
    if (chunkSize <= 0) {
        throw new IllegalArgumentException("chunkSize <= 0"); //$NON-NLS-1$
    }
    this.service = BlobstoreServiceFactory.getBlobstoreService();
    this.info = new BlobInfoFactory().loadBlobInfo(key);
    if (info == null) {
        throw new IOException(MessageFormat.format(
            "BlobInfo({0}) is not found",
            key.toString()));
    }
    this.chunkSize = Math.min(chunkSize, BlobstoreService.MAX_BLOB_FETCH_SIZE);
    this.positionInBlob = 0;
    this.positionInChunk = 0;
    this.chunk = null;
    this.closed = false;
}

/**
 * インスタンスを生成する。
 * @param info 読みだす対象のBlobメタ情報
 * @param chunkSize 分割して読みだす個々のチャンクのバイト数
 * @throws IllegalArgumentException 引数に{@code null}が含まれる場合、
 *     または{@code chunkSize}に0以下の値が指定された場合
 */
public BlobInputStream(BlobInfo info, int chunkSize) {
    if (info == null) {
        throw new IllegalArgumentException("info is null"); //$NON-NLS-1$
    }
    if (chunkSize <= 0) {
        throw new IllegalArgumentException("chunkSize <= 0"); //$NON-NLS-1$
    }
    this.service = BlobstoreServiceFactory.getBlobstoreService();
    this.info = info;
    this.chunkSize = Math.min(chunkSize, BlobstoreService.MAX_BLOB_FETCH_SIZE);
    this.positionInBlob = 0;
    this.positionInChunk = 0;
    this.chunk = null;
    this.closed = false;
}

/**
 * インスタンスを生成する。
 * @param service 実際にBlobの内容を読み出すサービス
 * @param info 読みだす対象のBlobメタ情報
 * @param chunkSize 分割して読みだす個々のチャンクのバイト数
 * @throws IllegalArgumentException 引数に{@code null}が含まれる場合、
 *     または{@code chunkSize}に0以下の値が指定された場合
 */
BlobInputStream(BlobstoreService service, BlobInfo info, int chunkSize) {
    if (info == null) {
        throw new IllegalArgumentException("info is null"); //$NON-NLS-1$
    }
    if (chunkSize <= 0) {
        throw new IllegalArgumentException("chunkSize <= 0"); //$NON-NLS-1$
    }
    this.service = service;
    this.info = info;
    this.chunkSize = Math.min(chunkSize, BlobstoreService.MAX_BLOB_FETCH_SIZE);
    this.positionInBlob = 0;
    this.positionInChunk = 0;
    this.chunk = null;
    this.closed = false;
}

@Override
public int read() throws IOException {
    checkAvailable();
    if (restInBlob() == 0) {
        return -1;
    }
    prepareChunk();
    int c = chunk[positionInChunk] & 0xff;
    advance(1);
    return c;
}

@Override
public int read(byte[] b, int off, int len) throws IOException {
    checkAvailable();
    if (b == null) {
        throw new IllegalArgumentException("b is null"); //$NON-NLS-1$
    }
    if (len == 0) {
        return 0;
    }
    if (restInBlob() == 0) {
        return -1;
    }
    int read = (int) Math.min(restInBlob(), len);
    int cursor = off;
    int rest = read;
    while (rest > 0) {
        assert restInBlob() > 0;
        prepareChunk();
        int readInChunk = Math.min(rest, restInChunk());
        System.arraycopy(
            chunk, positionInChunk,
            b, cursor,
            readInChunk);
        advance(readInChunk);
        rest -= readInChunk;
        cursor += readInChunk;
    }
    assert rest == 0;
    return read;
}

@Override
public int available() throws IOException {
    checkAvailable();
    return restInChunk();
}

@Override
public long skip(long n) throws IOException {
    checkAvailable();
    if (n < 0) {
        throw new IllegalArgumentException();
    }
    if (n == 0) {
        return 0;
    }
    long skip = Math.min(n, restInBlob());
    advance(skip);
    return skip;
}

@Override
public void close() throws IOException {
    chunk = null;
    closed = true;
}

private void advance(long count) {
    assert count <= restInBlob();
    positionInBlob += count;
    if (chunk != null) {
        if (count < restInChunk()) {
            positionInChunk += (int) count;
        }
        else {
            chunk = null;
            positionInChunk = 0;
        }
    }
}

private void prepareChunk() throws IOException {
    assert restInBlob() > 0;
    if (chunk != null) {
        assert positionInChunk < chunk.length;
        return;
    }
    long endIndex = Math.min(info.getSize(), positionInBlob + chunkSize);
    LOG.debug("Preparing chunk: name={}, size={}, range=[{}, {})", new Object[] {
            info.getFilename(),
            info.getSize(),
            positionInBlob,
            endIndex
    });
    try {
        chunk = service.fetchData(
            info.getBlobKey(),
            positionInBlob, endIndex);
        positionInChunk = 0;
    }
    catch (Exception e) {
        throw new IOException(e);
    }
    assert chunk.length > 0 : positionInBlob;
}

private int restInChunk() {
    if (chunk == null) {
        return 0;
    }
    return chunk.length - positionInChunk;
}

private long restInBlob() {
    return info.getSize() - positionInBlob;
}

private void checkAvailable() throws IOException {
    if (closed) {
        throw new IOException(MessageFormat.format(
            "{0} for {1} is already closed",
            getClass().getSimpleName(),
            info.getBlobKey()));
    }
}
}
追記 (2011.01.14 KATO)
1.3.5以降からBlobstoreInputStreamが提供されていますので、そちらをお使いください。

2010年4月8日木曜日

XCodeのプロジェクト構成

まずXCodeによる開発を行った場合のプロジェクト構成をまとめてみる。XCodeではプロジェクトの構成は必ずしも実ディレクトリと一致していない。XCode上で見える構成はグループという実ファイルに対する参照で取り扱われてる。とりあえずグループ:実ディレクトリの構成で書いてみる。

Classes:${basedir}/Classes/${projectName}
ここには作成するアプリのコードを置く。Delegateクラス、ViewControllerクラスなどなど。
Other Sources:${basedir}
ブートストラップとなるmain.m、共通のヘッダファイルとなる.pchなどはここに置く。CやC++のソースもここに置くのが慣習らしい。アプリケーション全体に関連するものを置く場所なのかな。
Resources:${basedir}
名前の通りリソースファイルを置くところ。画像、音声データや国際化対応用のファイルなどなど。mavenでいうとろこの"src/main/resources"。
Frameworks:実体は無し
アプリが利用するフレームワークが置かれる。EclipseでいうところのJRE_CONTAINERとかMAVEN2_CLASSPATH_CONTAINER。
Products:${basedir}/build
ビルド成果物が置かれる。mavenでいうところのtargetディレクトリ。

この構成の各グループの意味についてはiPhone SDK アプリケーション開発ガイドで知ることが出来た。