GAE 開発環境でQuery(“__Stat_Kind__”)を偽装する
Query(“__Stat_Kind__”) はGoogleの環境では使えるけれども開発環境では使えない。getSchemaは開発環境で使えるけれどもGoogleの環境では使えない。両方にmakeSyncCallしたいときはどうするの?っと。そこで開発環境にQuery(“__Stat_Kind__”)が来たときは、内部でgetSchemaしてQueryResultな結果を返すことにしました。仕様を見たわけではないので、確実に動作するかわりません。参考にする場合は注意してください。
1. Query.countEntities()
Query.countEntities()の結果はbyte[]で以下のいずれかになっています。
- [8][byte count] (0<=count<2^8)
- [16][short count] (2^8<=count<2^16)
- [32][int count] (2^16<=count<2^32)
最初の1バイトがQueryResponse本体のbit-length、そのあとにEntityの数。例えば次のような関数を考えます。
byte[] statKindCountEntitiesQueryResultEmu(int count){
if(count<256){
ByteBuffer buffer = ByteBuffer.allocate(2);
buffer.put((byte)8);
buffer.put((byte)count);
return buffer.array();
}
if(count<256*256){
ByteBuffer buffer = ByteBuffer.allocate(3);
buffer.put((byte)16);
buffer.putShort((short)count);
return buffer.array();
}
if(count<256*256*256*256){
ByteBuffer buffer = ByteBuffer.allocate(5);
buffer.put((byte)32);
buffer.putInt(count);
return buffer.array();
}
return new byte[0];
}
2. FetchOptions
FetchOptions fetchOptions; fetchOptions = FetchOptions.Builder.withOffset(0).limit(10); List<Entity> list = datastore.prepare(query).asList(fetchOptions);
というリクエストの場合、makeSyncCallサーブレットでは次のようにするとoffsetとlimitの値を取ることができます。
Query query = new DatastorePb.Query(); query.parseFrom(requestBytes); int offset = query.getOffset(); int limit = query.getLimit();
3. GetSchema
開発環境でkind一覧を取得するために、getSchemaを使用します。
Schema getSchema(){
GetSchemaRequest schemaRequest = new GetSchemaRequest();
schemaRequest.setApp(ApiProxy.getCurrentEnvironment().getAppId());
@SuppressWarnings("unchecked")
byte[] responseBytes = ApiProxy.getDelegate().makeSyncCall(
ApiProxy.getCurrentEnvironment(),"datastore_v3",
"getSchema", schemaRequest.toByteArray());
Schema schema = new Schema();
schema.mergeFrom(responseBytes);
return schema;
}
ここで取得したschemaの中身は
kind <
key <
app: "(app-id)"
path <
Element {
type: "(kind-name)"
}
>
>
entity_group <
>
property <
...
>
property <
...
>
...
>
4. Query(“__Stat_Kind__”)
実際の環境でQuery(“__Stat_Kind__”)を実行すると、以下の内容がkindの数だけ続きます。
result <
key <
app: "(app-id)"
path <
Element {
type: "__Stat_Kind__"
name: "(kind-name)"
}
>
>
entity_group <
Element {
type: "__Stat_Kind__"
name: "(kind-name)"
}
>
property <
name: "bytes"
value <
int64Value: ...
>
multiple: false
>
property <
name: "count"
value <
int64Value: ...
>
multiple: false
>
property <
meaning: 7
name: "timestamp"
value <
int64Value: 0x...
>
multiple: false
>
property <
name: "kind_name"
value <
stringValue: "(kind-name)"
>
multiple: false
>
>
結果をこの形式で返せばいいので、QueryResultを組み立てる関数は
byte[] statKindQueryResultEmu(Schema schema, int offset, int limit){
QueryResult queryResult = new DatastorePb.QueryResult();
List<EntityProto> kinds = schema.kinds();
if(offset<0) offset = 0;
if(limit<0) limit = kinds.size();
EntityProto entityProto;
Element statKindElm;
String kind;
for (int i = offset; i < offset+limit; i++) {
if(i>=kinds.size())break;
entityProto = kinds.get(i);
kind = entityProto.getKey().getPath().getElement(0).getType();
statKindElm = new OnestoreEntity.Path.Element();
statKindElm.setType("__Stat_Kind__");
statKindElm.setName(kind);
//set key
entityProto.getKey().getPath().clearElement();
entityProto.getKey().setApp(ApiProxy.getCurrentEnvironment().getAppId());
entityProto.getKey().getPath().addElement(statKindElm);
entityProto.getEntityGroup().addElement(statKindElm);
//set kind name
Property kindProperty = entityProto.addProperty();
kindProperty.setName("kind_name");
kindProperty.setValue(new PropertyValue().setStringValue(kind));
com.google.appengine.api.datastore.Query countQuery;
countQuery = new com.google.appengine.api.datastore.Query(kind);
countQuery.setKeysOnly();
DatastoreService service;
service = DatastoreServiceFactory.getDatastoreService();
int entitySize = service.prepare(countQuery).countEntities();
//set count
Property countProperty = entityProto.addProperty();
countProperty.setName("count");
countProperty.setValue(new PropertyValue().setInt64Value(entitySize));
queryResult.addResult(entityProto);
}
queryResult.setKeysOnly(false);
queryResult.setMoreResults(false);
return queryResult.toByteArray();
}
以上のことから開発環境のmakeSyncCallサーブレットでQuery(“__Stat_Kind__”)を偽装する方法は次のようになります。ただQuery(“__Stat_Kind__”)とQuery(“__Stat_Kind__”).countEntities()のリクエストはバイト単位で同じものだったので、リクエストからこれらを区別する方法がわかりませんでした。そこで実際のリクエストをApiProxyに投げて、その結果をもとに処理を分けています。
String environment = System.getProperty(
"com.google.appengine.runtime.environment");
if(environment.equals("Development")){
Query query = new DatastorePb.Query();
query.parseFrom(requestBytes);
if(query.getKind().equals("__Stat_Kind__")){
Schema schema = getSchema();
byte[] bytes = ApiProxy.makeSyncCall(
serviceName, methodName, requestBytes);
QueryResult qr = new QueryResult();
qr.parseFrom(bytes);
byte[] responseBytes;
if(qr.hasCursor()){
responseBytes = statKindQueryResultEmu(
schema, query.getOffset(), query.getLimit());
}else{
responseBytes = statKindCountEntitiesQueryResultEmu(
schema.kinds().size());
}
resp.setContentType("application/octet-stream");
resp.addHeader("Content-Length", String.valueOf(responseBytes.length));
resp.getOutputStream().write(responseBytes);
resp.getOutputStream().flush();
return;
}
}
t