RestletでJSONを扱う

GAE/JでRestlet(server-side)を使い、JSONをやり取りするようにしてみました。使用したRestletのバージョンは、Version 2.1 Snapshot (testing)です。

jarファイル

RestletにはGAE/J版が用意されているので、それをダウンロードし、配備します。配布されているアーカイブには、Restlet本体の他にorg.restlet.ext.*パッケージが含まれているのですが、今回はその中から以下のjarファイルを使用しています。

  • org.restlet.jar
  • org.restlet.ext.servlet.jar
  • org.restlet.ext.json.jar
web.xml

ServletはRestletのServerServletを使うようにします。RestletにはApplicationのサブクラスを作ってルーティングを設定し、各ServerResourceを呼び出すようにするのが基本的な流れになります。

    	<servlet>
    		<servlet-name>RestletServlet</servlet-name>
    		<servlet-class>org.restlet.ext.servlet.ServerServlet</servlet-class>
        	<init-param>
			<param-name>org.restlet.application</param-name>
			<param-value>net.wrap_trap.lifecycle.rest.LifecycleRestApplication</param-value>
		</init-param>
	</servlet>
	<servlet-mapping>
		<servlet-name>RestletServlet</servlet-name>
		<url-pattern>/*</url-pattern>
	</servlet-mapping>
Application

ルーティング等を設定します。

public class LifecycleRestApplication extends Application {
 
    @Override
    public Restlet createInboundRoot() {
        Router router = new Router(getContext());
        getConnectorService().getClientProtocols().add(Protocol.FILE);
 
        Directory dir = new Directory(getContext(), LocalReference.createFileReference(new File("war/")));
        router.attachDefault(dir);
        router.attach("/events/{year}/{month}/{day}", DailyLogResource.class);
 
        return router;
    }
}

上記コードは以下の2点を設定しています。

  • ルーティングのデフォルト(どのルーティング定義にも合致しない場合)の振る舞いを設定
  • リクエスURIが「/events/yyyy/MM/dd」であれば、DailyLogResourceにルーティングする
    • URIに指定された{year},{month},{day}の各値は、UniformResource#getRequestAttributesで取得することができる
ServerResource

ServerResourceのサブクラスを作成し、任意のリソースを管理します。今回は以下の振る舞いを行うServerResourceを作成しました。

  • GET
    • yyyy/MM/ddに対応するリソースをBigTableから取り出し、JSONに変換してクライアントに返す
  • PUT
    • リクエストボディのJSONを受け取り、Javaのオブジェクトに変換し、BigTableに書き込む
    • @Put("json")
      • リクエストのContent-Typeがapplication/jsonの時のみ対応する
public class DailyLogResource extends ServerResource {

	public DailyLogResource() {
		Set<Method> allowedMethods = new HashSet<Method>();
		allowedMethods.add(Method.GET);
		allowedMethods.add(Method.PUT);
		setAllowedMethods(allowedMethods);
	}
	
	@Get
	@Transaction
	public JsonRepresentation getDailyLog(){
		Date date = getDate();
		Collection<TimeLog> timeLogList = null;
		DailyLog dailyLog = DailyLog.getDailyLog(date);
		if(dailyLog != null && dailyLog.getTimeLogs() != null && dailyLog.getTimeLogs().size() > 0){
			timeLogList = DailyLog.detachCopyAll(dailyLog.getTimeLogs());
		}else{
			timeLogList = Collections.EMPTY_LIST;
		}
		JSONArray jsonArray = new JSONArray();
		for(TimeLog timeLog : timeLogList){
			jsonArray.put(timeLog.toJSON());
		}
		return new JsonRepresentation(jsonArray);
	}
	
	@Put("json")
	@Transaction
	public JsonRepresentation saveOrUpdateDailyLog(Representation rep) throws JSONException, IOException{
		JsonRepresentation jsonRep = new JsonRepresentation(rep.getText());
		Date date = getDate();
		DailyLog dailyLog = DailyLog.getDailyLog(date);
		if(dailyLog == null){
			dailyLog = DailyLog.createDailyLog(date);
			dailyLog.save();
		}
		List<TimeLog> timeLogs = dailyLog.getTimeLogs();		
		
		JSONArray jsonArray = jsonRep.getJsonArray();
		for(int i = 0; i < jsonArray.length(); i++){
			JSONObject calEvent = jsonArray.getJSONObject(i);
			TimeLog timeLog = null;
			if(calEvent.isNull("encodedKey")){
				// insert
				timeLog = new TimeLog();
				timeLog.setDailyLog(dailyLog);
				timeLogs.add(timeLog);
			}else{
				// update
				timeLog = TimeLog.getObjectById(TimeLog.class, calEvent.get("encodedKey"));
			}
			timeLog.setStart(calEvent.getLong("startLong"));
			timeLog.setEnd(calEvent.getLong("endLong"));
			timeLog.setName(calEvent.getString("title"));
			timeLog.setOrder(0);
			timeLog.save();
			calEvent.put("encodedKey", timeLog.getEncodedKey());
		}
		return new JsonRepresentation(jsonArray);
	}
	
	protected Date getDate(){
		Map<String, Object> attrMap = getRequestAttributes();
		String year = (String)attrMap.get("year");
		String month = (String)attrMap.get("month");
		String day = (String)attrMap.get("day");
		
		Calendar cal = Calendar.getInstance();
		cal.set(Integer.parseInt(year), Integer.parseInt(month), Integer.parseInt(day));
		return cal.getTime();
	}
}

ServerResourceのサブクラスのインスタンスはリクエスト毎に作成されるので、インスタンスフィールドを使うことができます。GET/PUTに関わらず、クライアントにデータを返す場合は、各メソッドの戻り値をRepresentationにします。本来であればMediaTypeを見て適切なRepresentationのサブクラスを選ぶのが正しいのですが、今回はJSONしか扱わないので所々でJSON決め打ちのコードとなっています。
ちょっと惜しいのが、JsonRepresentationを生成する際に複数のオブジェクトが格納されたListを指定すると、JSONとして出力されるのはListのプロパティ(empty)のみになってしまうこと。今回はListに対応するJSONArrayを作成し、Listに格納されているJavaオブジェクトを自前でJSONObjectに変換してから詰めるようにすることで、JSONで返せるようにしています。RestletでガッツリJSONを扱うのであれば、指定したJavaオブジェクトを再帰的にJSONObjectに変換してくれるようなRepresentationのサブクラスを作った方が良いでしょう。

まとめ

RestletでJSONを扱ってみました。Restletは思ったよりも情報が少なく、目的どおりに動かすのが少し難しいかもしれません。しかし、ルーティングの設定やRestletの導入自体は簡単なので、一般的なWebアプリケーションで使うというのも面白いと思います。