StoryboardではTableViewのセルのカスタマイズも1画面でできるようになりました。
セルのプロトタイプから動的にセルを生成する
カスタムセルのプロトタイプを作って、プログラムで動的にセルの内容を決定するTableViewを作ってみます。
最初に、プロジェクトを作るところから始めます。Master Detail Applicationテンプレートから新規プロジェクトを作ります。
StoryboardではTableViewのセルのカスタマイズも1画面でできるようになりました。
カスタムセルのプロトタイプを作って、プログラムで動的にセルの内容を決定するTableViewを作ってみます。
最初に、プロジェクトを作るところから始めます。Master Detail Applicationテンプレートから新規プロジェクトを作ります。
Xcode4.2からStoryboadというものが使えるようになりました。いままで画面ごとにnibファイルを作ってプログラムで画面遷移することができましたが、Storyboardを使えば各画面の関係を1画面で見渡すことができます。また、単純な画面遷移ならプログラムを一切書かずに作ることができます。
ボタンを押したら次の画面に移動するだけの簡単な画面遷移を作ってみます。
まずはプロジェクトの作成から。
File –> New –> New Project でSingle View Applicationを選択します。
新規プロジェクトのプロジェクト名などを入力する画面で「Use Storyboard」のチェックを入れて、プロジェクトを作成します。
AndroidのWi-Fiを使ってpingする方法です。
まずはゲートウェイのアドレスを調べます。DhcpInfoのgatewayフィールドに入っています。
WiFiManager wifi = (WifiManager)getSystemService(WIFI_SERVICE);
DhcpInfo dhcpInfo = wifi.getDhcpInfp();
int gateway = dhcpInfo.gateway;
ここにはリトルエンディアンで入っています。これをビッグエンディアンの配列にします。
byte[] gatewayAddr = new byte[] {
(byte)(gateway&255),
(byte)((gateway>>8)&255),
(byte)((gateway>>16)&255),
(byte)((gateway>>24)&255)
};
あとは普通のJavaプログラムと同様にpingします。
InetAddress addr = InetAddress.getByAddress(gatewayAddr);
if ( addr.isReachable(3000) ) {
…
}
GalaxyS Android2.3.3のバッテリー問題。突然バッテリーの消費が激しくなり、8時間くらいでバッテリーを完全消費してしまう。
この問題、どうやら日本だけで起こっているわけではないらしい。
http://www.google.mk/support/forum/p/Google+Mobile/thread?tid=1ea61a8c2fffdb2e&hl=en
システムモニタのアプリで調べてみたら、原因はシステムプロセスのsuspend。アプリではない。
下のスクリーンショットを見ると、端末がスリープに入るとsuspendプロセスがCPUを使い始めていることがわかる。
いまのところ、バッテリー消費が激しくなったら再起動するしかないようだ。
ワットチェッカーで測ってみた。ワットチェッカーは1W単位でしか測れないので測定値はかなり大雑把な値。仕事に使っていても大体4~5時間くらいは使えるので夏の電力不足の時期には重宝しそう。
ディスプレイ輝度最大 10W
ディスプレイ輝度最小 7W
ディスプレイ輝度中間(10段階のうち下から5番目) 8W
最大26W (ディスプレイ輝度中間)
0W (小さすぎて測れませんでした。1W未満ということ。)
データのクラスに@Modelアノテーションを付ける。
永続化するプロパティには getter, setter が必要。
永続化しないフィールドには@Attribute(persistent = false) アノテーションを付ける。
フィールドで使える型はDatastoreの基本型、基本型のコレクション、シリアライズ可能なオブジェクト。
フィールドで使える基本型 → Core Value Types
シリアライズ可能なオブジェクトはBlob型として格納される(@Attribute(lob = true)を付ける)。
500文字(byte[]は500バイト)を超えるフィールドには@Attribute(lob = true)を付ける。
配列はコレクションとして扱われない(シリアライズしてBlobになる?)。
インデックスに対応した型はデフォルトでインデックスが付く。
インデックスを付けたくないフィールドには@Attribute(unindexed = true)を付ける。
ArrayList, LinkedList, HashSet, LinkedHashSet, TreeSet, List (ArrayList), Set (HashSet), SortedSet (TreeSet)
()内はプロパティから返される実際のインスタンスの型。
@Attribute(listener = ???) アノテーションで値を自動更新するリスナを指定できる。
保存するたびに更新するときは、
@Attribute(listener = ModificationDate.class)
Date updatedAt;
最初の保存のときだけ更新するときは、
@Attribute(listener = CreationDate.class)
Date createdAt;
モデルクラスに1つだけ主キープロパティを持たなければならない。
キーのフィールドには @Attribute(primaryKey = true) を付ける。
キーは パス、kind、ID によって構成される。ネームスペースを使っているときはネームスペース名もキーに保存される。詳しくは「MultitenancyとNamespaces API」を参照。
kind はクラスの単純名から付けられる。kindはクラスの @Model(kind = "...") アノテーションで変更可能。
IDはアプリケーションで指定した文字列か、Datastoreが付けた数値(キーをnullのまま保存)。
自分で作ったIDからキーを作るとはき Datastore.createKey(…)。IDの自動割り当てでキーを作るときは Datastore.allocateId(…)。キーを作成したらモデルオブジェクトの主キープロパティにセットする。主キーをセットせずに Datastore.put するとキーは自動生成される。
ModelRef型のプロパティを作って次のように初期化とgetterメソッドを作成。
@Model public class Address { … }
@Model public class Employee {
private ModelRef<Address> addressRef =
new ModelRef<Address>(Address.class);
public ModelRef<Address> getAddressRef() { return addressRef; }
}
インスタンスを作成して関連付けして保存
Address address = new Address();
Employee employee = new Employee();
employee.getAddressRef().setModel(address);
Datastore.put(address, employee);
ロードするときは、
Employee employee = Datastore.get(Employee.class, employeeKey);
Address address = employee.getAddressRef().getModel();
参照先はレイジーロードされる。
先ほどの関連に逆方向の参照を追加。逆方向はInverseModelRef型にする。逆方向の参照は永続化しない。Addressクラスに以下のコードを追加。
@Attribute(persistent = false)
private InverseModelRef<Employee, Address> employeeRef =
new InverseModelRef<Employee, Address>
(Employee.class, "addressRef", this);
public InverseModelRef<Employee, Address> getEmployeeRef() {
return employeeRef;
}
これでAddressからEmployeeへの参照ができる。
address.getEmployeeRef.getModel();
プロパティに関連をセットするには正方向側(Employee側)のプロパティにセットする。逆方向にはsetModel()メソッドが無い。
多 → 1 の参照を持つ関連の場合、片方向1対1の場合と同じ。
多 → 1への参照に加え、1 → 多 への参照を追加。1側から多側への参照は永続化しない。
@Attribute(persistent = false)
private InverseModelListRef<Employee, Department> employeeListRef =
new InverseModelListRef<Employee, Department>
(Employee.class, "departmentRef", this);
public InverseModelListRef<Employee, Department> getEmployeeListRef()
{…}
1側から多側を取得する場合は次のようにする。
List<Employee> employeeList = department.getEmployeeListRef()
.getModelList();
関連クラスを作って、1 ← 多(関連クラス) → 1 のようにする。
関連クラスに1側への参照プロパティをそれぞれ作る。
@Model
public class EmployeeProject {
…
private ModelRef<Employee> employeeRef =
new ModelRef<Employee>(Employee.class);
private ModelRef<Project> projectRef =
new ModelRef<Project>(Project.class);
public ModelRef<Employee> getEmployeeRef() {…}
public ModelRef<Project> getProjectRef() {…}
…
}
1側は"双方向多対1"の逆参照と同様。EmployeeProjectに対して永続化しない InverseModelListRef<EmployeeProject, 1側クラス> のプロパティとgetterを作る。
ロードするときは片側(たとえばProjectオブジェクト)から関連オブジェクトのリストを取得して、関連オブジェクトから反対側のオブジェクトを得る。
relObject = project.getEmployeeProjectRef().getModelList();
relObject.getEmployeeRef().getModel();
アプリ指定の値で作成
Datastore.createKey(…);
値を自動割り当て
Datastore.allocateId(…);
親エンティティを指定して作成
Datastore.createKey(parentKey, Child.class, value);
Datastore.allocateId(parentKey, Clind.class);
org.slim3.datastore.DataStore.put(object);
非同期での保存も可能。
DataStore.putAsync(object);
複数のオブジェクトの保存はコレクションか配列をputに渡すか、putに複数のパラメータを並べる
Datastore.get(Class, Key);
複数のオブジェクトの取り出しもできる。
DataStore.get(Class, コレクション or 配列);
Datastore.get(Class, key1, key2, …);
オブジェクトを更新するときは、ロード(get)、オブジェクトの値の変更、保存(put)の順で行う。
Datastore.delete(Key);
ロードや更新と同様、複数オブジェクトを同時に削除できる。
子孫エンティティごと削除するには
Datastore.deleteAll(Key);
データクラスをEclipseで作成すると、自動的にメタデータが作成される。クエリの作成の際にこのメタデータを使う。
DataClassのメタデータのインスタンスは次のように取得できる。
DataClassMeta meta = DataClassMeta.get();
EntityQuery query = Datastore.query(meta);
List results = query.filter(
meta.property1.equal(value1),
meta.progerty2.greaterThan(value2), …
).asList();
インメモリでフィルタ。
endsWith や contains が使える。
query.filterInMemory(meta.property.endsWith("XYZ"))
query.sort(meta.property1.asc, meta.property2.desc)
インメモリでソート
query.sortInMemory(meta.property.asc)
query.offset(5).limit(10)
query.asList()
query.asIterator()
query.asSingle()
query.asKeyList()
query.asKeyIterator()
[TODO] 後で調べる
List<Child> list = Datastore.query(Child.class, ancestorKey)
.asList();
[TODO] 後で調べる
List<Entity> list = Datastore.query(ancestorKey).asList();
不等式フィルタはクエリ中で1つのプロパティだけにしか使用できない。
不等式で使われたプロパティは最初のソート項目に無ければならない(ソートを指定しなければならない)。
フィルタで指定したプロパティを持っていないエンティティは、クエリに引っかからない。
インデックスされないプロパティはクエリに引っかからない。
Text、Blob、@Attribute(unindexed = true) アノテーションをつけたプロパティはインデックスされない。
同じプロパティ名で別の型の値を持つエンティティがあると、型でソートされたあと値でソートされる。
対応するインデックス定義が無いクエリは失敗する。
単純なクエリに対するインデックスは自動作成される。
インデックスはxmlで定義。
クエリに必要なインデックス定義は開発環境でクエリを実行したときに自動的に作成される。
手作業で定義するときは WEB-INF/datastore-indexes.xml に書く。
datastore-indexes.xml の autoGenerate が true になっている場合、WEB-INF/appengine-generated/datastore-indexes-auto.xml にインデックスが自動生成される。
カーソルとは、フェッチ処理後の次の位置を表す文字列。
S3QueryResultList を通じてカーソルを使用する。
S3QueryResultList results = query.limit(20).asQueryResultList();
results.getEncodedCursor();
results.getEncodedFilters();
results.getEncodedSorts();
results.hasNext();
query.encodedCursor(encodedCursor)
.encodedFilters(encodedFilters)
.encodedSorts(encodedSorts)
.limit(20).asQueryResultList();
エンティティの親子関係はキーを作成するときに親エンティティのIDを指定することで決定する
プロパティに他のエンティティの参照を持ってもエンティティグループに入れるわけではない
エンティティの親子関係はあとで変えることはできない
先祖エンティティが削除されても子孫エンティティは削除されない
Transaction tx = Datastore.beginTransaction();
DataClass data = Datastore.get(tx, Dataclass.class, key);
data.setProperty(value);
Datastore.put(tx, data);
tx.commit();
単一のトランザクションでできることは
1つのトランザクション内では1つのエンティティグループの操作だけができる
(エンティティのロードも含めて)。
楽観的並列処理なのでトランザクションが失敗したらアプリ側で何度かリトライする。
トランザクションの外側のトランザクション分離性はRead committedに近いがトランザクションはSerializableで実行される。
トランザクション内の祖先クエリやgetはトランザクション内での変更があってもトランザクション開始時点のスナップショットを返す。
トランザクションを超える更新(Web画面のためのGETリクエストの後、更新のためのPOSTリクエストでDatastoreにputするなど)を行う場合、データのバージョンにより競合を検出できる。
エンティティに @Attribute(version = true) を付けたプロパティを用意することで、書き込みためのgetの際にすでに他の書き込みによってバージョンが変更されていたら ConcurrentModificationException が投げられる。
Datastore.get(Transaction, Class, Key, version)
まず、Datastore が HRD で動作するよう設定する。
次にappengine-web.xml の system-properties要素に
<property name="slim3.useXGTX" value="true"/>
を加える。これがないとローカル環境でXGが使えない。
あとは通常通りトランザクションを使用するだけで1つのトランザクションで複数のグループを扱うことができる。
複数のエンティティグループにまたがったトランザクションを実行できる。
GlobalTransaction gtx = Datastore.beginGlobalTransaction();
gtx.get(…);
gtx.put(…);
gtx.commit();
同じエンティティグループに対してローカルトランザクションとグローバルトランザクションを混ぜて使ってはいけない。グローバルトランザクションはパフォーマンス的にはローカルトランザクションと大きな違いはないそうなので、グローバルトランザクションを使う可能性があればそのエンティティグループの操作には常にグローバルトランザクションを使うと良い。
Multitenancyとは1つのアプリケーションインスタンスが複数のクライアント(ユーザーグループ)に対してサービスを行うソフトウェアアーキテクチャ。
ドキュメントによは、次のような用途が記載されている。
つまりうっかり混ざってほしくないデータを分けて保存したい場合に使えるようだ。Namespace APIを使ってネームスペース設定することでデータをネームスペースごとに分けて保存することができる。
ネームスペースが使えるのは
だけ。
Blobstoreではネームスペースは使えない。
Multitenancyは Namespaces APIを使って実現する。使い方は簡単で、NamespaceManager.set(String) メソッドを呼ぶだけでカレントネームスペースを設定できる。
ネームスペースに設定できる文字列は100文字までの英数字、'-'、'_'、'.'で構成された文字列。アンダーバーで始まる文字列はシステム予約なので使えない。
ネームスペースを設定しなければカレントネームスペースはnullで、この場合各種AppEngine APIは空文字列("")のネームスペースを使用する。
ネームスペースはリクエストごとに設定する必要がある。各リクエストが開始された時点ではカレントネームスペースは設定されていない。
ネームスペースで分離したデータがネームスペースを超えて漏洩しないように注意しなければならない。ネームスペースは文字列ひとつで設定できるのでうっかりすればネームスペースを超えたデータにアクセスできてしまう。
Blobsoreはネームスペースをサポートしないので、ネームスペースを使うアプリケーションでは、blobキーをネームスペースつきのDatastoreに保存してこのDatastoreデータを経由してblobにアクセスする。ブラウザから渡されたblobキーで直接blobにアクセスしてしまうと分離したいデータにアクセスできてしまうかもしれない。
Datastore APIを呼び出す前にNamespaceManagerでカレントネームスペースを設定しておくだけで良い。
KeyやQueryを作るとき、APIはカレントネームスペースを参照してKey、Queryオブジェクトにネームスペースを設定する。先祖を指定してキーを作成すると、新しいキーは先祖キーのネームスペースを継承する。
なお、KeyやQueryに明示的にネームスペースを設定するJava APIは無い。
Key、Queryオブジェクトをシリアライズしたデータにはネームスペースも含まれる。これらをデシリアライズして使うときにはネームスペースが適切かどうか注意する必要がある。
カレントネームスペースが”a”のときに作られたキーは、シリアライズされてカレントネームスペース”b”でデシリアライズされてもキーのネームスペースは”a”のままで復元される。これをDatastoreにストアするとネームスペース”a”にストアされる。
信用できないソース(ブラウザなど)から渡されたKeyでDatastoreにアクセスするとネームスペースを超えてデータにアクセスしてしまう危険があるので、受け取ったキーのネームスペースが適切かどうか検証してから使用すること。
MemcacheService を作成するときに明示的にネームスペースを設定しなければ、MemcacheService はメソッドが呼ばれるときにカレントネームスペースの設定を参照して memcache にアクセスする。
下のコード例はドキュメントから引用。
// Create a MemcacheService that uses the current namespace by
// calling NamespaceManager.get() for every access.
MemcacheService current =
MemcacheServiceFactory.getMemcacheService();
// stores value in namespace "abc"
String oldNamespace = NamespaceManager.get();
NamespaceManager.set("abc");
try {
current.put("key", value); // stores value in namespace “abc”
} finally {
NamespaceManager.set(oldNamespace);
}
MemcacheServiceFactory.getMemcacheService(String) で明示的にネームスペースを指定すると、カレントネームスペースを無視して指定したネームスペースで memcache にアクセスする。
MemcacheService boundMemcache =
MemcacheServiceFactory.getMemcacheService("specific-namespace");
NamespaceManager.set("whatever-namespace");
// このレコードはネームスペース"specific-namespace"でストアされる。
boundMemcache.put("key3", "value3");
タスクが作られるときにNamespaceManagerにセットしたカレントネームスペースとGoogle Appsドメインも(もしあれば)共にキューにプッシュされる。
タスクが実行されるとき、カレントネームスペースとGoogle Appsドメインが復元される。もしタスクをaddするときにカレントネームスペースが設定されていなければ、タスクが実行されるとき空のネームスペースが設定される。
タスク名はネームスペースで分離されないのですべてのネームスペースに渡ってユニークな名前にしないとぶつかる。
Queueからタスクをプルする場合はネームスペースの機能は提供されない。自前でタスクのネームスペースを復元できるようにペイロードにネームスペースを入れるなどの策が必要。
カレントネームスペースを操作する機能を提供する。
カレントネームスペースはget()によって返される文字列。Datastore、Memcache、Task QueueのAPIで使われる。
ネームスペースに関連したクラス(例えば、Key、Query、MemcacheService)が作られるとき、ネームスペースが決まっていなければget()の呼び出しで決定される。もしget()がnullを返したらカレントネームスペースはセットされておらずこれらのAPIは空の("")ネームスペースを使う。
例:
NamespaceManager.set("a-namespace");
MemcacheService memcache =
MemcacheServiceFactory.getMemcacheService();
// Store record in namespace "a-namespace"
memcache.put("key1", "value1");
NamespaceManager.set("other-namespace");
// Store record in namespace "other-namespace"
memcache.put("key2", "value2");
MemcacheService boundMemcache =
MemcacheServiceFactory.getMemcacheService("specific-namespace");
NamespaceManager.set("whatever-namespace");
// このレコードはネームスペース"specific-namespace"でストアされる。
boundMemcache.put("key3", "value3");
MemcacheService memcache (上記の例で)はカレントネームスペースを使う。そして key1 がネームスペース「a-namespace」にストアされる。key2 はネームスペース「other-namespace」にストアされる。カレントネームスペースに優先してデータを特定のネームスペースにストアもできる。上記の例で key3 はネームスペース「specific-namespace」にストアされる。
Task Queueの Queue.add() メソッドは追加されるタスクの中にNamaspaceManagerの設定を転送し、追加されたタスクはタスクを作ったものと同じカレントネームスペースで実行される。
例外的に、カレントネームスペースが設定されていないとき(つまりget()がnullを返すとき)は空のネームスペース("")が作られたタスクのリクエストに転送される。
メソッド
public static void set(java.lang.String newNamespace)
ネームスペースに関連したサービスのネームスペース初期化に使われる値をセットする。
Parameters:
newNamespace - 新しいネームスペース。
Throws:
java.lang.IllegalArgumentException - ネームスペース文字列が正しくないとき。
public static java.lang.String get()
カレントネームスペース設定またはnull(設定されていないとき)返す。
もしカレントネームスペースが設定されていなければ、呼び出し元は ネームスペースに関連したすべてのサービスで空のネームスペース(“”)を使うべきである。
public static java.lang.String getGoogleAppsNamespace()
このリクエストのGoogle Appsドメインを返す。または、空の文字列を返す。
public static void validateNamespace(java.lang.String namespace)
ネームスペース文字列の妥当性を検証する。
Throws:
java.lang.IllegalArgumentException - ネームスペース文字列のフォーマットが正しくないとき。
特定のURLパターンに対してアクセスを制限するには web.xml のsecurity-constraint要素で定義できる。
web-app要素(ルートの要素)の下にsecurity-constraint要素を追加する。
<security-constraint>
<web-resource-collection>
<web-resource-name>admin tool</web-resource-name>
<url-pattern>/admin-tool*</url-pattern>
</web-resource-collection>
<user-data-constraint>
<role-name>admin</role-name>
</user-data-constraint>
</security-constraint>
アクセス可能なロールをadminとしておくと、AppEngineアプリの設定画面で管理者として設定したGoogleアカウントでしかアクセスできなくなる。Googleにログインせずに該当のURLにアクセスした場合は自動的にGoogleアカウントへのログイン画面へリダイレクトされる。
開発環境とGAEのサーバー環境でアクセス制限方法を変えたり、非公開バージョンだけにアクセス制限をかけたい場合など、security constraint では柔軟性に欠ける場合にはアプリ自前のコードでアクセス制限をかけることができる。ただし、静的ファイルにはこの方法は使えない。
ここで言う”管理者”もsecurity-constraintの場合と同じ、アプリの設定画面で設定したGoogleアカウントのことを指す。
次のAPIはアクセス制限の役に立つ。
SystemProperty.applicationId
リクエストされたホスト名がアプリのホスト名かどうか判断するときに使う。
SystemProperty.environment
アプリが稼動する環境が開発環境かGAEサーバー環境かを判断するときに使う。
UserService#isUserAdmin()
Googleアカウントにログイン中のユーザーがアプリの管理者かどうかを判断するときに使う。
ユーザーがログインしているかを判断するときに使う。
コード例
UserService userService = UserserviceFactory.getUserService();
if ( request.getUserPrincipal() != null
&& userService.isUserAdmin() ) {
// アクセス許可
}
else {
// アクセス拒否
}
App EngineのアプリのビルドはEclipseプラグインを使うと簡単だが、Antを使うこともできる。
以下、Google App Engineのドキュメントに沿って説明する。コード例などはドキュメントから引用。
App Engineアプリの典型的なディレクトリ構成は次のとおり。これ以降このディレクトリ構成を前提にする。
プロジェクトルート/
src/
パッケージ/Javaソースコード
META-INF/
設定ファイルなど
war/
Web用のファイル (JSP, 画像などの静的ファイル)
WEB-INF/
アプリの設定ファイルなど
classes/
classファイル
lib/
ライブラリのJAR
build.xmlには以下の設定をする。
App EngineのSDKの場所。自分の環境に合わせて設定する。
<property name="sdk.dir" location="../appengine-java-sdk" />
Antマクロのインポート。
<import file="${sdk.dir}/config/user/ant-macros.xml" />
コンパイルに必要なJARをクラスパスに追加する。以下の場所をクラスパスに追加する。
<path id="project.classpath">
<pathelement path="war/WEB-INF/classes" />
<fileset dir="war/WEB-INF/lib">
<include name="**/*.jar" />
</fileset>
<fileset dir="${sdk.dir}/lib">
<include name="shared/**/*.jar" />
</fileset>
</path>
App Engine SDKのJARをwar/WEB-INF/libにコピーする。
<target name="copyjars"
description="Copies the App Engine JARs to the WAR.">
<copy
todir="war/WEB-INF/lib"
flatten="true">
<fileset dir="${sdk.dir}/lib/user">
<include name="**/*.jar" />
</fileset>
</copy>
</target>
普通にコンパイルするだけ。
まず、コンパイルしたclassファイルを入れるディレクトリを作成。次に出力ディレクトリにリソースをコピー(Javaのソースファイルはコピーから除外)。そしてコンパイル。
<target name="compile" depends="copyjars"
description="Compiles Java source and copies other source files to the WAR.">
<mkdir dir="war/WEB-INF/classes" />
<copy todir="war/WEB-INF/classes">
<fileset dir="src">
<exclude name="**/*.java" />
</fileset>
</copy>
<javac
srcdir="src"
destdir="war/WEB-INF/classes"
classpathref="project.classpath"
debug="on" />
</target>
JDOを使わないならこのプロセスは不要。
JDOを使うためにはJDOアノテーションをつけたクラスに対してコンパイル後のclassファイルに後処理が必要。
App EngineのAntマクロ<enhance_war>を使う。
<target name="datanucleusenhance" depends="compile"
description="Performs JDO enhancement on compiled data classes.">
<enhance_war war="war" />
</target>
開発用サーバもAntマクロで起動できる。
<target name="runserver" depends="datanucleusenhance"
description="Starts the development server.">
<dev_appserver war="war" />
</target>
より詳細な設定もできる。サーバの起動ポート、JavaVMの引数を指定する方法はドキュメントによると以下のとおりだが、このデバッグオプションを指定するとなぜか正常にJettyが起動しなかった。
<dev_appserver war="war" port="8888" >
<options>
<arg value="--jvm_flag=-Xdebug"/>
<arg value="--jvm_flag=-Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=9999"/>
</options>
</dev_appserver>
サーバを停止するには Ctrl + C を押すとドキュメントにはあるが、これではAndのプロセスが止まるだけでサーバのプロセスは止まっていないようなので、Windowsの場合ならタスクマネージャーでプロセスを終了させる必要がある。
<appcfg>マクロを使う。これはApp Engine SDKのツールAppCfgを呼び出すためのマクロ。パラメータなど詳細はAppCfgのドキュメントを参照。
<target name="update" depends="datanucleusenhance"
description="Uploads the application to App Engine.">
<appcfg action="update" war="war" />
</target>
アプリのアップデート以外のAppCfgは次のとおり。
datastoreのインデックスを更新する。アプリをアップデートする前にインデックスだけ更新するときに使う。
<target name="update_indexes" depends="datanucleusenhance"
description="Uploads just the datastore index configuration to App Engine.">
<appcfg action="update_indexes" war="war" />
</target>
ロールバック。アプリの更新が部分的に完了した場合に元に戻す。
<target name="rollback" depends="datanucleusenhance"
description="Rolls back an interrupted application update.">
<appcfg action="rollback" war="war" />
</target>
ログのダウンロード。
<target name="request_logs"
description="Downloads log data from App Engine for the application.">
<appcfg action="request_logs" war="war">
<options>
<arg value="--num_days=5"/>
</options>
<args>
<arg value="logs.txt"/>
</args>
</appcfg>
</target>
ここまでの説明で登場したターゲットの実行順は次のようになっている。
copyjars → compile → datanucleusenhance → runserver
[省略] → datanucleusenhance → update
[省略] → datanucleusenhance → update_indexes
[省略] → datanucleusenhance → rollback
request_logs
BlobstoreはDatastoreより大きなblobオブジェクトを扱うことができる。BlobはHTTPリクエストでファイルをアップロードすることで作られる。ファイルがアップロードされるとBlobstoreがblob keyを返す。blogを操作するためにこのキーを使う。
blobのサイズは最大で2GB。
Blobstoreのblobは、Datastoreのblobプロパティとは別のもの。
blobは削除することはできるが変更することはできない。
blobの作成時刻やコンテントタイプなどの情報はDatastoreに記録される。blobキーはblobの情報を読み出すクエリに使うことができる。
アプリはblobに直接アクセスできない。かわりにDatastoreのblob情報エンティティを通してblobを扱える。
ユーザはHTMLフォームの1つまたは複数のファイル入力フィールドからblobを作ることができる。
アプリはblobstoreService.createUploadUrl()で得られるURLをフォームのactionにセットする。このメソッドにはアプリのハンドラのパスを渡す。ユーザはBlobstoreに直接ファイルをアップロードする。
Blobstoreはユーザのリクエストを書き換えてアップロードされたファイルをストアし、blobキーに対応するファイルデータを置き換える。そして書き換えられたリクエストをあなたがcreateUploadUrl()に提供したURLパスのハンドラに渡す。
ハンドラはblobキーに基づいた追加の処理ができる。
最後に、ハンドラはヘッダのみのリダイレクトレスポンス(301, 302, 303)を返して、典型的にはブラウザは、blobアップロードのステータスを示す別のページにリダイレクトする。
アプリはファイルのようなストリーミングインタフェースを使ってBlobstoreのデータを読むことができる(BlobstoreInputStream)。
フォームの例。
<form action="<%= blobstoreService.createUploadUrl("/upload") %>"
method="post" enctype="multipart/form-data">
<input type="file" name="myFile">
<input type="submit" value="Submit">
</form>
actionのURLはcreateUploadUrl()で作成。メソッドにはアップロード完了で呼ばれるハンドラのURLを渡す。
フォームにはファイルinputのフィールドを含む必要がある。enctypeは"multipart/form-data"にする。
ハンドラが呼ばれるときにはすでにblobは保存され、Datastoreにblob情報ができている。
ハンドラでアップロードされたblobのリストを取り出す例。Mapには<ファイル名,BlobKey>の組で入っている。ファイル名はアップロードの際にフォームに書いた値。
Map<String, BlobKey> blobs = blobstoreService.getUploadedBlobs(req);
blobstoreがユーザリクエストを書き換えるとき、アップロードされたファイルのMIMEパートはbodyが空にして、MIMEパートのヘッダにblobキーを追加する。他のすべてのフォームフィールドとパートはそのままハンドラに渡される。
コンテントタイプを指定しない場合、Blobstoreはファイルの拡張子からコンテントタイプを推定する。コンテントタイプが決定されないときは、新しく作られたblobは application/octet-streamが割り当てられる。
blobを供給するためにはアプリでblobのダウンロードハンドラを用意する必要がある。ハンドラでは欲しいblobのキーをblobstoreService.serve(blobKey, res)メソッドに渡す。
例:
BlobKey blobKey = new BlobKey(req.getParameter("blob-key"));
blobstoreService.serve(blobKey, res);
blobはアプリのどんなURLからも供給できる。アプリでblobを供給するために、blobキーを含んだ特別なヘッダをレスポンスに含める。App Engineはレスポンスのボディをblobの内容で置き換える。
レスポンスにX-AppEngine-BlobRangeヘッダを含めることで大きなデータの一部だけを標準的なHTTPバイトレンジで返すことができる。
X-AppEngine-BlobRangeをブランクにするとレンジヘッダを無視して完全なblobを返す。
範囲指定の例:
バイト範囲が正しければBlobstoreはクライアントに"206 Partial Content"のステータスコードと要求されたバイト範囲を送る。もしレンジが正しくなければBlobstoreは"416 Requested Range Not Satisfiable"を送る。
Blobstoreは1つのリクエストで複数のバイト範囲をサポートしない。
イメージサービスはBlobstoreのデータを変換のソースとして使用できる。ソースイメージはBlobstoreデータの最大サイズまで扱える。変換されたイメージは1MBより小さくなければならない。
詳しくはイメージサービスのドキュメントを参照。
プログラムからblobデータを読み書きするためのAPIが用意されている。この機能によってデータをエクスポートしたり、生成したバイナリデータを保存したりできる。
つきのようにして書き込み可能な空のファイルを作成し、書き込み可能な Channel をオープンする。
AppEngineFile file =
fileService.createNewBlobFile("text/plain");
FileWriteChannel writeChannel =
fileService.openWriteChannel(file, lock);
たとえ1バイトを書くだけでも、それぞれの書き込みが最小のCPUクォータを消費する。書き込みをバッチ処理することによって、効率的にクォータを使うことができる。
(訳注:この「書き込み」の単位がメソッド呼び出しなのかファイルをクローズするまでなのかは不明。)
Blobstoreデータに使われたスペースは保存データのクォータ(課金対象)に計上される。
Datastoreに含まれるblob情報エンティティがDatastore関連のクォータに計上される。
Blobstoreの操作によって消費されたCPU時間はCPU時間のクォータ(課金対象)に計上される。
詳しくはクォータの情報はクォータと管理コンソールの「クォータ詳細情報」のドキュメントを参照。
リミット
Channel APIはアプリケーションとgoogleサーバーの間に持続的な接続を作り、JavaScriptクライアントにポーリングを使わずにリアルタイムにメッセージを送ることができる。
クライアントの役割
サーバの役割
クライアントIDはサーバでそれぞれのJavaScriptクライアントを識別するためのもの。クライアントIDはアプリケーションでどのように設計しても良い。
トークンはJavaScriptクライアントがチャネルに接続して待ち受けることを可能にする役割がある。
サーバはそれぞれのクライアントのためにクライアントIDと有効期限などの情報を使って トークンを作成する。
チャネルはサーバーがクライアントIDで識別されるJavaScriptクライアントにアップデートを送るための片方向の通信路。
サーバはクライアントからPOSTによってアップデートを受信すると、該当するクライアントにチャネルを使ってメッセージを送る。
メッセージはクライアントからサーバにPOSTによって送られる。
受信すると、サーバはクライアントIDで識別されたクライアントにチャネルを使ってメッセージを渡す。
メッセージは32KBまで。
URLを送るときに生のメッセージで送るのは避ける。その代わりにメッセージが損なわれずに到達するようにJSONエンコーディングを使う。
JavaScriptクライアントはサーバから提供されたトークンを使ってソケット開く。チャネルで更新を待ち受けるためにソケットを使う。
channelServiceを取得して、チャネルを作成。トークンを生成する。
ChannelService channelService =
ChannelServiceFactory.getChannelService();
String token = channelService.createChannel(clientId);
サーバからもらったトークンを使ってチャネルを開く。
channel = new goog.appengine.Channel(token);
socket = channel.open();
socket.onopen = onOpened;
socket.onmessage = onMessage;
socket.onerror = onError;
socket.onclose = onClose;
channelService.sendMessage(
new ChannelMessage(clientId, messageString));
createChannel()によって返されるトークンは他に知られないように秘密にしておく。もしトークンが盗まれたらチャネルに送られたメッセージを待ち受けできてしまう。
トークンは2時間で期限切れになる。クライアントが2時間以上チャネルに接続したままでいると、ソケットのonerror(), onclose()がコールバックされる。
1つのクライアントIDで一度にチャネルに接続できるのは1つのクライアントだけ。アプリケーションは1つのクライアントIDでメッセージを多数のクライアントにばら撒くことはできない。
クライアントは1ページにつき1つのチャネルにだけ接続できる。
もしアプリケーションがクライアントに複数の種類のデータを送る必要があるならサーバサイドでデータを集めてクライアントのソケットのonmessageハンドラに送る。
チャネルに接続したクライアントには"プレゼンス"の概念がない。これはクライアントのチャネルへの接続または切断についてアプリケーションに通知されないことを意味する。
もし、アプリケーションがクライアントの接続を追跡する必要があるなら、1つの方法としてクライアントの状態を通知するために適当な間隔でPOSTメッセージをサーバに送るよう設計された関数をソケットのonopenプロパティにセットする。
この記事はこちらへ移動しました。
「Slim3でDatastoreを使う」
アプリ指定の値で作成
Datastore.createKey(…);
値を自動割り当て
Datastore.allocateId(…);
親エンティティを指定して作成
Datastore.createKey(parentKey, Child.class, value);
Datastore.allocateId(parentKey, Clind.class);
org.slim3.datastore.DataStore.put(object);
非同期での保存も可能。
DataStore.putAsync(object);
複数のオブジェクトの保存はコレクションか配列をputに渡すか、putに複数のパラメータを並べる
Datastore.get(Class, Key);
複数のオブジェクトの取り出しもできる。
DataStore.get(Class, コレクション or 配列);
Datastore.get(Class, key1, key2, …);
オブジェクトを更新するときは、ロード(get)、オブジェクトの値の変更、保存(put)の順で行う。
Datastore.delete(Key);
ロードや更新と同様、複数オブジェクトを同時に削除できる。
子孫エンティティごと削除するには
Datastore.deleteAll(Key);
データクラスをEclipseで作成すると、自動的にメタデータが作成される。クエリの作成の際にこのメタデータを使う。
DataClassのメタデータのインスタンスは次のように取得できる。
DataClassMeta meta = DataClassMeta.get();
EntityQuery query = Datastore.query(meta);
List results = query.filter( meta.property1.equal(value1), meta.progerty2.greaterThan(value2), … ).asList();
インメモリでフィルタ。
endsWith や contains が使える。
query.filterInMemory(meta.property.endsWith("XYZ"))
query.sort(meta.property1.asc, meta.property2.desc)
インメモリでソート
query.sortInMemory(meta.property.asc)
query.offset(5).limit(10)
query.asList()
query.asIterator()
query.asSingle()
query.asKeyList()
query.asKeyIterator()
[TODO] 後で調べる
List<Child> list = Datastore.query(Child.class, ancestorKey).asList();
[TODO] 後で調べる
List<Entity> list = Datastore.query(ancestorKey).asList();
不等式フィルタはクエリ中で1つのプロパティだけにしか使用できない。
不等式で使われたプロパティは最初のソート項目に無ければならない(ソートを指定しなければならない)。
フィルタで指定したプロパティを持っていないエンティティは、クエリに引っかからない。
インデックスされないプロパティはクエリに引っかからない。
Text、Blob、@Attribute(unindexed = true) アノテーションをつけたプロパティはインデックスされない。
同じプロパティ名で別の型の値を持つエンティティがあると、型でソートされたあと値でソートされる。
対応するインデックス定義が無いクエリは失敗する。
単純なクエリに対するインデックスは自動作成される。
インデックスはxmlで定義。
クエリに必要なインデックス定義は開発環境でクエリを実行したときに自動的に作成される。
手作業で定義するときは WEB-INF/datastore-indexes.xml に書く。
datastore-indexes.xml の autoGenerate が true になっている場合、WEB-INF/appengine-generated/datastore-indexes-auto.xml にインデックスが自動生成される。
カーソルとは、フェッチ処理後の次の位置を表す文字列。
S3QueryResultList を通じてカーソルを使用する。
S3QueryResultList results = query.limit(20).asQueryResultList();
results.getEncodedCursor();
results.getEncodedFilters();
results.getEncodedSorts();
results.hasNext();
query.encodedCursor(encodedCursor)
.encodedFilters(encodedFilters)
.encodedSorts(encodedSorts)
.limit(20).asQueryResultList();
Transaction tx = Datastore.beginTransaction();
DataClass data = Datastore.get(tx, Dataclass.class, key);
data.setProperty(value);
Datastore.put(tx, data);
tx.commit();
単一のトランザクションでできることは
1つのトランザクション内では1つのエンティティグループの操作だけができる
(エンティティのロードも含めて)。
楽観的並列処理なのでトランザクションが失敗したらアプリ側で何度かリトライする。
トランザクションの外側のトランザクション分離性はRead committedに近いがトランザクションはSerializableで実行される。
トランザクション内の祖先クエリやgetはトランザクション内での変更があってもトランザクション開始時点のスナップショットを返す。
トランザクションを超える更新(Web画面のためのGETリクエストの後、更新のためのPOSTリクエストでDatastoreにputするなど)を行う場合、データのバージョンにより競合を検出できる。
エンティティに @Attribute(version = true) を付けたプロパティを用意することで、書き込みためのgetの際にすでに他の書き込みによってバージョンが変更されていたら ConcurrentModificationException が投げられる。
Datastore.get(Transaction, Class, Key, version)
複数のエンティティグループにまたがったトランザクションを実行できる。
GlobalTransaction gtx = Datastore.beginGlobalTransaction();
gtx.get(…);
gtx.put(…);
gtx.commit();
同じエンティティグループに対してローカルトランザクションとグローバルトランザクションを混ぜて使ってはいけない。グローバルトランザクションはパフォーマンス的にはローカルトランザクションと大きな違いはないそうなので、グローバルトランザクションを使う可能性があればそのエンティティグループの操作には常にグローバルトランザクションを使うと良い。
この記事はこちらへ移動しました。
「Slim3でDatastoreを使う」
データのクラスに@Modelアノテーションを付ける
永続化するプロパティには getter, setter が必要
永続化しないフィールドには@Attribute(persistent = false) アノテーションを付ける
フィールドで使える型はDatastoreの基本型、基本型のコレクション、シリアライズ可能なオブジェクト
フィールドで使える基本型 → Core Value Types
シリアライズ可能なオブジェクトはBlob型として格納される(@Attribute(lob = true)を付ける)
500文字(byte[]は500バイト)を超えるフィールドには@Attribute(lob = true)を付ける
配列はコレクションとして扱われない(シリアライズしてBlobになる?)
インデックスに対応した型はデフォルトでインデックスが付く
インデックスを付けたくないフィールドには@Attribute(unindexed = true)を付ける
ArrayList, LinkedList, HashSet, LinkedHashSet, TreeSet, List (ArrayList), Set (HashSet), SortedSet (TreeSet)
()内はプロパティから返される実際のインスタンスの型
@Attribute(listener = ???) アノテーションで値を自動更新するリスナを指定できる
保存するたびに更新
@Attribute(listener = ModificationDate.class)
Date updatedAt;
最初の保存のときだけ更新
@Attribute(listener = CreationDate.class)
Date createdAt;
モデルクラスに1つだけキーを持たなければならない
キーのフィールドには @Attribute(primaryKey = true) を付ける
キーは パス、kind、ID によって構成される
kind はクラスの単純名から付けられる
kindはクラスの @Model(kind = "...") アノテーションで変更可能
IDはアプリケーションで指定した文字列か、Datastoreが付けた数値(キーをnullのまま保存)
キーは Datastore.createKey(Class, String) で作成してキープロパティにセットする
エンティティの親子関係はキーを作成するときに親エンティティのIDを指定することで決定する
プロパティに他のエンティティの参照を持ってもエンティティグループに入れるわけではない
エンティティの親子関係はあとで変えることはできない
先祖エンティティが削除されても子孫エンティティは削除されない
ModelRef型のプロパティを作って次のように初期化とgetterメソッドを作成。
@Model public class Address { … }
@Model public class Employee {
private ModelRef<Address> addressRef = new ModelRef<Address>(Address.class);
public ModelRef<Address> getAddressRef() { return addressRef; }
}
インスタンスを作成して関連付けして保存
Address address = new Address();
Employee employee = new Employee();
employee.getAddressRef().setModel(address);
Datastore.put(address, employee);
ロードするときは、
Employee employee = Datastore.get(Employee.class, employeeKey);
Address address = employee.getAddressRef().getModel();
参照先はレイジーロードされる。
先ほどの関連に逆方向の参照を追加。逆方向はInverseModelRef型にする。逆方向の参照は永続化しない。Addressクラスに以下のコードを追加。
@Attribute(persistent = false)
private InverseModelRef<Employee, Address> employeeRef = new InverseModelRef<Employee, Address>(Employee.class, "addressRef", this);
public InverseModelRef<Employee, Address> getEmployeeRef() { return employeeRef;}
これでAddressからEmployeeへの参照ができる。
address.getEmployeeRef.getModel();
プロパティに関連をセットするには正方向側(Employee側)のプロパティにセットする。逆方向にはsetModel()メソッドが無い。
多 → 1 の参照を持つ関連の場合、片方向1対1の場合と同じ。
多 → 1への参照に加え、1 → 多 への参照を追加。1側から多側への参照は永続化しない。
@Attribute(persistent = false)
private InverseModelListRef<Employee, Department> employeeListRef = new InverseModelListRef<Employee, Department>(Employee.class, "departmentRef", this);
public InverseModelListRef<Employee, Department> getEmployeeListRef() {…}
1側から多側を取得する場合は次のようにする。
List<Employee> employeeList = department.getEmployeeListRef().getModelList();
関連クラスを作って、1 ← 多(関連クラス) → 1 のようにする。
関連クラスに1側への参照プロパティをそれぞれ作る。
@Model
public class EmployeeProject {
…
private ModelRef<Employee> employeeRef = new ModelRef<Employee>(Employee.class);
private ModelRef<Project> projectRef = new ModelRef<Project>(Project.class);
public ModelRef<Employee> getEmployeeRef() {…}
public ModelRef<Project> getProjectRef() {…}
…
}
1側は"双方向多対1"の逆参照と同様。EmployeeProjectに対して永続化しない InverseModelListRef<EmployeeProject, 1側クラス> のプロパティとgetterを作る。
ロードするときは片側(たとえばProjectオブジェクト)から関連オブジェクトのリストを取得して、関連オブジェクトから反対側のオブジェクトを得る。
relObject = project.getEmployeeProjectRef().getModelList();
relObject.getEmployeeRef().getModel();
Anddroid端末の解像度を取得する方法。
下のコードで画面のいろいろな情報を取得できる。
DisplayMetrics mtr = new DisplayMetrics();
context.getWindowManager().getDefaultDisplay().getMetrics(mtr);
GalaxySの画面の実寸サイズは長辺86mm 800px, 短辺52mm 480pxなので、長辺234.46153846153846153846153846154 dpi, 短辺 236.27906976744186046511627906977 dpi。
GalaxyS上でAPIで得られる数値はXdpi 234.46153, Ydpi 236.27907 なので実寸と同じ。こういうのはあまり信用できない数値が返ってくることが多いけど、試してみたGalaxySでは正確な数値が得られるようだ。
参考ページ
PackageManager の queryIntentActivities(Intent intent, int flags) メソッドで取得できる。
PackageManagerのインスタンスはActivityからgetPackageManager()メソッドで取得できる。
受け取って欲しいインテントを intentパラメータに指定する。flagsは
MATCH_DEFAULT_ONLY
GET_INTENT_FILTERS
GET_RESOLVED_FILTER
を指定できるが、フラグを設定しなくても(0を指定)動作するようだ。android.intent.category.DEFAULT のインテントを探すなら MATCH_DEFAULT_ONLY を指定する。
参考ページ
コードを貼り付けながら。「アプリケーションリストを取得する」
Y.A.M の 雑記帳「Android 特定の Intent (Action) を処理できる Activity (アプリ)の一覧を取得」
「開発の基礎」の「アクティビティとタスク」の節がなかなか理解できなかったのでまとめ。
アクティビティのスタック。ブラウザでいえばHistoryみたいなもの。タスクそれぞれにアクティビティのスタックがある。
タスクはブラウザのタブみたいなもの。ただし、タブのように "見える化" されていなくてタスクに含まれるアプリケーションを再び起動しようとしたときにそのアプリケーションのタスクがフォアグラウンドになる。ただし、インテントのflagプロパティやアクティビティの起動モード定義によって挙動は変わる。
アクティビティは自身と同じAffinityが設定されているタスクに所属しようとする。
タスクのAffinityはルートのアクティビティによって決まる。
アクティビティのAffinityはAndroidManifest.xmlのactivity要素のtaskAffinity属性で設定(たぶん任意の文字列で指定可能)。指定がなければ、application要素のtaskAffinity属性を継承する。アプリケーションのAffinityのデフォルトはmanifest要素のパッケージ名。
インテントにFLAG_ACTIVITY_NEW_TASK フラグが設定されている場合、新しいアクティビティは別のタスクに所属しようとするが、そのアクティビティと同じAffinityが設定されている既存のタスクがあればそこに追加される。なければ新しいタスクが開始される。
[あとで検証] もしFLAG_ACTIVITY_NEW_TASK フラグを設定したインテントで新しいアクティビティを起動するとき、startActivity()を呼び出したタスクと新しいアクティビティに同じAffinityが設定されていたら新しいタスクが起動するかどうか。たぶん新しいタスクはできないと予想。
activity要素に allowTaskReparenting="true" と設定されていると、このアクティビティと同じAffinityが設定されているタスクがフォアグラウンドに移ったときに、アクティビティを開始したタスクからそのタスクに移動できる。たとえば taskAffinity="A" のタスクで taskAffinity="B" のアクティビティを開始すると B は A のタスクに属するが、taskAffinity="B" のアプリケーションを起動すると B のアクティビティがこのタスクに移動する。
activity要素の launchMode属性で定義。
"standard"
"singleTop"
"singleTask"
"singleInstance"
後述するが、アプリケーションのメインアクティビティには singleTask が向いているはずだが、実際は上記のような挙動にならないため standard に設定しておくと良い。
ユーザーがタスクを長時間放置したときは、ルート以外のアクティビティがクリアされる。この挙動はactivity要素の属性で変更できる。
alwaysRetainTaskState = "true"
アクティビティはクリアされない。
clearTaskOnLaunch = "true"
タスクを離れるとルートを含めた全てのアクティビティがクリアされる。
finishOnTaskLaunch = "true"
タスクを離れるとこのアクティビティはクリアされる。それがルートであっても。
Intent に FLAG_ACTIVITY_CLEAR_TOP フラグを設定すると、インテントを処理するアクティビティのインスタンスが対象タスクのスタック内に存在する場合、そのインスタンスより上位(新しい方)のアクティビティはすべてクリアされる。
startActivityForResult() で呼び出したアクティビティがクリアされてしまった場合、呼び出し元アクティビティの onActivityResult() が resultCode = RESULT_CANCELED で呼ばれる。
アクティビティのインテントフィルタのアクションを android.intent.action.MAIN、カテゴリを android.intent.category.LAUNCHER に設定すると、アクティビティのアイコンとラベルがアプリケーションランチャに表示され、アプリケーションのエントリポイントになる。
ドキュメントでは、この設定をしたアクティビティはユーザーが他のタスクに移動したあとでアプリケーションに戻ってこられるように、起動モード singleTask か singleInstance を設定したほうが良いとされているが、実際にやってみると起動モード singleTask ではルートアクティビティ以外がクリアされた状態で起動してしまう。なぜか起動モード standard のタスクではスタックが保持されたまま元のタスクに戻ることができた(Android 1.6, 2.2で検証)。