コーディングで気をつけること

sastrutsは開発者に取って非常に扱いやすいフレームワークで、JAVA初心者でも割りと早く実装ができるフレームワークだと思います。

ただ仕様のせいと売り文句のせいでスパゲティコードを量産してしまうことがあります。

今日は経験からのメモ

actionのプロパティ

sastrutsではactionのpublicフィールドをプロパティとしてリクエストスコープにいれJSPで利用できる様にしています。
確かにリクエストオブジェクトを利用しないで簡潔に書けるのでいいのですが、ここがかなりスパゲティコードを量産します。

よくやるのが[〜表示フラグ]

画面のある部分の表示制御の為に作ったフラグがいつの間にかクラス全域で使われ、更には他の条件判定に使われまた新たなフラグが作られ…

そんなプログラムのバク対応をした日にゃ…

かき途中

T2+GAE+guice+AIR 多分その3 認証処理を作ってみる。

まえのエントリーでも書きましたが、
T2はその拡張性の高さも使いやすい理由の一つだと思います。
T2では、各処理のポイントに拡張する方法を提供しているので、
非常に扱いやすのです。

拡張を差し込むという意味では、インターセプターもいますが、
利用してみたところ、動きが見えずらい、いつ動くの?
との声が多く結構難しんだな〜と思いました。

てことで、T2のPluginを使って、認証処理を作成してみました。

実装

前回のエントリーにも書いてますが、若干GuiceAdapterを拡張してますので、
あしからず。

まずAnnotationです。
Pageクラスのクラスまたはメソッドにつけて、
認証するかしないかを判断します。

package jp.stk.gae.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE , ElementType.METHOD} )
public @interface Auth {
	public boolean onlyAdmin() default false;
	public boolean canUseNonUser() default false;
}

次にPluginです。

抽象クラス:AuthPlugin

package jp.stk.gae.plugin;

import org.t2framework.t2.plugin.AbstractPlugin;

public abstract class AuthPlugin extends AbstractPlugin {

}

実装クラス:AuthPluginImpl

package jp.stk.gae.plugin.impl;

import java.lang.reflect.Method;

import javax.servlet.http.HttpServletRequest;

import jp.stk.gae.annotation.Auth;
import jp.stk.gae.exception.AuthException;
import jp.stk.gae.plugin.AuthPlugin;

import org.t2framework.commons.meta.MethodDesc;
import org.t2framework.commons.util.Logger;
import org.t2framework.commons.util.StringUtil;
import org.t2framework.t2.action.ActionContext;
import org.t2framework.t2.contexts.HttpMethod;
import org.t2framework.t2.spi.Navigation;

import com.google.appengine.api.users.UserService;
import com.google.inject.Inject;

public class AuthPluginImpl extends AuthPlugin {
	@SuppressWarnings("unused")
	private final static Logger logger = Logger.getLogger(AuthPluginImpl.class);

	@Inject
	UserService userService;

	@Inject
	HttpServletRequest request;

	@Override
	public Navigation beforeActionInvoke(ActionContext actionContext,
			MethodDesc targetMethod, Object page, Object[] args) {
		Method m = targetMethod.getMethod();
		Class<?> c = m.getDeclaringClass();
		Auth classAnn = c.getAnnotation(Auth.class);
		Auth methodAnn = m.getAnnotation(Auth.class);
		if (classAnn == null && methodAnn == null) {
			return super.beforeActionInvoke(actionContext, targetMethod, page,
					args);
		}

		Auth auth = methodAnn != null ? methodAnn : classAnn;

		if (!userService.isUserLoggedIn()) {
			addLoginLink2Request(actionContext);
			if (auth.onlyAdmin()) {
				throw new AuthException("この画面は利用できません。");
			}
			if (!auth.canUseNonUser()) {
				throw new AuthException("ログインしていない方は、利用できません。");
			}
			return super.beforeActionInvoke(actionContext, targetMethod, page,
					args);
		}
		addLogoutLink2Request(actionContext);
		if (auth.onlyAdmin() && !userService.isUserAdmin()) {
			throw new AuthException("この画面は利用できません。");
		}

		return super.beforeActionInvoke(actionContext, targetMethod, page, args);
	}

	private void addLoginLink2Request(ActionContext actionContext) {
		if (actionContext.getRequest().isAjaxRequest()
				|| actionContext.getRequest().isAmfRequest()) {
			return;
		}

		String url = actionContext.getRequest().getRequestURI();
		String queryString = null;
		if (actionContext.getRequest().getMethod() != HttpMethod.GET) {
			url = "/";
			queryString = "";
		} else {
			queryString = request.getQueryString();
		}

		String redirectUrl = url;
		if (!StringUtil.isEmpty(queryString)) {
			redirectUrl += url.contains("?") ? "&" : "?";
			redirectUrl += queryString;
		}
		String loginUrl = userService.createLoginURL(redirectUrl);
		actionContext.getRequest().setAttribute("loginUrl", loginUrl);
	}

	private void addLogoutLink2Request(ActionContext actionContext) {
		if (actionContext.getRequest().isAjaxRequest()
				|| actionContext.getRequest().isAmfRequest()) {
			return;
		}

		String logoutUrl = userService.createLogoutURL("/");
		actionContext.getRequest().setAttribute("logoutUrl", logoutUrl);
	}
}


であとはModuleとかでこのPluginを設定して、
認証したい、ページにAuthアノテーションを付ければ
OKです。

T2+GAE+guice+AIR 多分その2 拡張してみる

T2はDIコンテナがいろいろ選べて、自分の好みのDIコンテナが使えます。
タイトルにもあるように、私のサイトではGoogle Guiceを利用しているのですが、
少々他のDIコンテナに比べて問題があります。
それはT2のもつ拡張性を半減させている点です。

Guiceを利用する場合、
他のDIコンテナ(といってもSeasarしかやってみてませんが。。。)と違い、
複数コンポーネントの取得ができません。

なんだそれって感じですが、これのせいでT2では以下の問題が出てきます。*1

  1. ExceptionHandlerが拡張できない(使えない)。
  2. Pluginが複数設定できない。

この二つは結構重要で、
たとえば、独自のException(認証例外とか)を作って、
それにより処理を変えたりができません。*2
またPluginにより、Pageクラス処理の前で、さまざまな事をしたい場合、
それを一つのメソッド内で終える必要があります。

じゃあどうすればよいか。。。
考えられるのは以下で、

  1. DIコンテナを変える。
  2. がまん
  3. 運用で回避
  4. Adapterを拡張する。

1.は無理じゃなくもないのですが、
すでにGuice向けに開発しているものを、
他の者に変えるのは若干リスクがあります。

2.は勉強目的もあるのでやです

3.は大手企業さんがいうことじゃないんだからありえません。

で4.です。
基本的な枠組みはすでにT2のGuiceAdapterにできているので、
これ自体を拡張します。

GuiceAdapterの拡張

問題点

まず何が問題かを考えると、
Guiceがあるクラス(インターフェース)に対して、
複数のクラスを定義できないことが問題です。*3
なので、上記の二つの問題が出てきます。

改善策

私のようなしょぼレベルで考えると要は、
1対多のリレーションが貼れないから、
この問題が起きるであれば、
1対多のリレーションを貼るものを作ればよいとおもいます。
かつGuiceAdapterでは最初から、
ModuleをServiceLoaderでファイルを読み込むので
同じようにファイルでそのリレーションを
作ってしまえばいいじゃん、と考えました。

実装

でとりあえず作ってみました。

package jp.stk.gae.adapter;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;

import org.apache.commons.io.IOUtils;
import org.apache.commons.io.LineIterator;
import org.t2framework.commons.util.CollectionsUtil;
import org.t2framework.commons.util.Reflections.ClassUtil;
import org.t2framework.t2.adapter.GuiceAdapter;
import org.t2framework.t2.handler.ExceptionHandler;
import org.t2framework.t2.plugin.Plugin;

public class StkGuiceAdapter extends GuiceAdapter {
        //インターフェースとクラスをひもづけるマップ
	protected Map<Class<?>, List<Class<?>>> componentsMap = CollectionsUtil
			.newConcurrentHashMap();

	@Override
	public void init() {
		super.init();
        //とりあえずT2のクラスだけリレーションを張る。
		componentsMap.put(ExceptionHandler.class, load(ExceptionHandler.class));
		componentsMap.put(Plugin.class, load(Plugin.class));
	}

	@SuppressWarnings("unchecked")
	@Override
	public <T> List<T> getComponents(Class<? super T> componentClass) {
        //所持していなければ、同じ動作
		if (!componentsMap.containsKey(componentClass)) {
			return super.getComponents(componentClass);
		}

		List<T> resultList = new ArrayList<T>();
         //リレーションマップより本当に紐づているクラスリストを取得
		List<Class<?>> classList = (List<Class<?>>)componentsMap.get(componentClass);
		for (Class<?> clazz : classList) {
			try {
				T t = (T) injector.getInstance(clazz);
				resultList.add(t);
			} catch (Throwable throwable) {
                //TODO もうちょっとかんがえる。
				throwable.printStackTrace();
				continue;
			}
		}
		if (resultList.isEmpty()) {
			return super.getComponents(componentClass);
		}
		return resultList;
	}

    //上の処理のExceptionHandler版
	@Override
	public List<ExceptionHandler<Throwable, Exception>> createExceptionHandlers() {
		try {
			return this.getComponents(ExceptionHandler.class);

		} catch (Throwable t) {
			return CollectionsUtil.emptyList();
		}
	};

    //ファイルからリレーションの作成処理
    //ServiceLoaderと同じで、引数のクラス名から、ファイル名を作成し、
    //ファイルより、ひもづけさせるクラスを取得する。
	@SuppressWarnings("unchecked")
	private <T> List<Class<?>> load(Class<T> serviceClass) {
         //一応スレッドセーフなりすと
        //TODO 必要ない?
		List<Class<?>> serviceList = new CopyOnWriteArrayList();

		try{
			LineIterator iterator = null;
			for(iterator = 
IOUtils.lineIterator(StkGuiceAdapter.class.getResourceAsStream("/META-INF/services/" + serviceClass.getName()) , "UTF-8")
					;iterator.hasNext();){
				String line = iterator.nextLine();
				Class<?> clazz = ClassUtil.forName(line);
				serviceList.add(clazz);
			}
		}catch(Exception e){
            //TODO やる気をだす
			throw new RuntimeException(e);
		}
		return serviceList;
	}

	@Override
	public void destroy() {
		super.destroy();
		componentsMap.clear();
		componentsMap = null;
	}
}

まだまだ改良中ですが、とりあえず動いているので、
あとはパフォーマンス考えて直します。

設定

まずModuleを作ります。

package jp.stk.gae.module;


import jp.stk.gae.handler.StkExceptionHandler;
import jp.stk.gae.plugin.AuthPlugin;
import jp.stk.gae.plugin.PagePlugin;
import jp.stk.gae.plugin.RequestPlugin;
import jp.stk.gae.plugin.impl.AuthPluginImpl;
import jp.stk.gae.plugin.impl.RequestPluginImpl;
import jp.stk.gae.service.CategoryService;
import jp.stk.gae.service.FuriganaService;
import jp.stk.gae.service.QuestionService;
import jp.stk.gae.service.impl.CategoryServiceImpl;
import jp.stk.gae.service.impl.FuriganaServiceImpl;
import jp.stk.gae.service.impl.QuestionServiceImpl;

import org.t2framework.t2.plugin.Plugin;

import com.google.inject.AbstractModule;
import com.google.inject.Scopes;
import com.google.inject.servlet.ServletModule;

public class ApplicationModule extends AbstractModule {

	@Override
	protected void configure() {
		install(new ServletModule());
		install(new JdoModule());
		install(new GaeServiceModule());
                //実際は、一つぐらいは基底クラス(ExceptionHandler)の実態クラスとして、定義しておいた方がよい
		bind(StkExceptionHandler.class).toInstance(new StkExceptionHandler());
        //こんな感じで
		bind(Plugin.class).to(PagePlugin.class).in(Scopes.SINGLETON);
		bind(AuthPlugin.class).to(AuthPluginImpl.class).in(Scopes.SINGLETON);
		bind(RequestPlugin.class).to(RequestPluginImpl.class).in(Scopes.SINGLETON);


		bind(QuestionService.class).toInstance(new QuestionServiceImpl());
		bind(FuriganaService.class).toInstance(new FuriganaServiceImpl());
		bind(CategoryService.class).toInstance(new CategoryServiceImpl());
	}
}

次に、META-INF/service/の下に、
ロード用のファイルを作成します。
ファイル:com.google.inject.Module

jp.stk.gae.module.ApplicationModule

ファイル:org.t2framework.t2.handler.ExceptionHandler

jp.stk.gae.handler.StkExceptionHandler

ファイル:org.t2framework.t2.plugin.Plugin

org.t2framework.t2.plugin.Plugin
jp.stk.gae.plugin.AuthPlugin
jp.stk.gae.plugin.RequestPlugin

これで、複数のPluginや、ExceptionHandlerが定義できるようになります。
もし、元になるファイルが、増えた場合はさらにそのリレーションを作る
ファイルを用意して、読み込ませればいいのかなーと思ってます。

例:
ファイル名:relation.map

org.t2framework.t2.plugin.Plugin
org.t2framework.t2.handler.ExceptionHandler

※注 いい設計ではないと思ってます。

サイトのほうはバックグランドのほうはできました。
あとはフロントの部分だけです。
これが一番時間がかかるんですけどね。。。

*1:他にもあるかもしれないですが

*2:GlobalExceptionHandlerを拡張することはできます

*3:Namedや独自アノテーションという方法もありますが...

T2+GAE+guice+AIR 多分その1

シルバーウィーク中に作るといっときながら様々な事にはまり、
まだまだ完成はできていないタイピングサイトです。

中途半端な状態ですが、とりあえず途中経過メモ

T2にPluginを設定してみる。

まず前回のエントリーまでの設定を行って、Pluginを設定してみます。

package jp.stk.gae.plugin;

import java.lang.reflect.Field;

import org.t2framework.commons.meta.MethodDesc;
import org.t2framework.commons.util.Logger;
import org.t2framework.t2.action.ActionContext;
import org.t2framework.t2.contexts.Request;
import org.t2framework.t2.plugin.AbstractPlugin;
import org.t2framework.t2.spi.Navigation;

public class PagePlugin extends AbstractPlugin {
    @SuppressWarnings("unused")
    private final static Logger logger = Logger.getLogger(PagePlugin.class);

	@Override
	public Navigation beforeActionInvoke(ActionContext actionContext,
			MethodDesc targetMethod, Object page, Object[] args) {
		return super.beforeActionInvoke(actionContext, targetMethod, page, args);
	}

	@Override
	public Navigation afterActionInvoke(ActionContext actionContext,
			MethodDesc targetMethod, Object page, Object[] args) {
		Class<?> t = targetMethod.getDeclaringClass();
		Request request = actionContext.getRequest();
		for (Field f : t.getFields()) {
			try {
				request.setAttribute(f.getName(), f.get(page));
			} catch (IllegalArgumentException e) {
			} catch (IllegalAccessException e) {
			}
		}
		return super.afterActionInvoke(actionContext, targetMethod, page, args);
	}

}

sastrutsのactionのパブリックフィールドがそのままリクエストに乗るっていうのが結構好きだったので、それをやってみました。
※本当は通信の方法によって分けたかったのですが、よくわかりませんでした。とりあえず、Pluginのお試しということで、たぶんすぐ消すかもです。

でこれをT2に読んでもらうために、guiceのモジュールに追加します。

package jp.stk.gae.module;


import jp.stk.gae.plugin.PagePlugin;
import jp.stk.gae.service.FuriganaService;
import jp.stk.gae.service.QuestionService;
import jp.stk.gae.service.impl.FuriganaServiceImpl;
import jp.stk.gae.service.impl.QuestionServiceImpl;

import org.t2framework.t2.plugin.Plugin;

import com.google.inject.AbstractModule;
import com.google.inject.servlet.ServletModule;

public class ApplicationModule extends AbstractModule {

	@Override
	protected void configure() {

		install(new ServletModule());
		bind(Plugin.class).toInstance(new PagePlugin());
	}
}

で動かせば、Pageクラスのパブリックフィールドが、Pageクラスの処理後に、リクエストにsetAttributeされ、Jspで呼び出せるようになります。
※あまりGuiceの仕様書は読んでないのでDI設計的にbindのやり方が、あってるか微妙です。

T2でAMF通信をしてみる。

タイピングアプリの問題取得をAIRを利用して、AMFによっておこなうようにします。
ちなみにAIRの実装はAmaterasのAIR GEARとFlashDevelopです。
FlexBuilderを買うお金はありません。。。(T_T)

ちなみにこれが最もハマりました。
そもそもはじめてのAMFかつほぼ未経験のActionScriptというのも原因なのですが、
泣きそうでした。

でとりあえず、まずJAVAソースです。
まず、AMFの通信を行う、Pageクラスのメソッドです。

	@SuppressWarnings("unchecked")
	@Amf
	public Navigation putQuestionList(final RequestDto requestDto) {
		List<Question> questionList = questionService.findAll();  //1
		if (questionList != null) {
			List<QuestionDto> questionDtoList = new ArrayList<QuestionDto>();
			for (Question question : questionList) {
				QuestionDto questionDto = new QuestionDto(); //2
				questionDto.setExplain(question.getExplain());
				questionDto.setExplainAlpha(question.getExplainAlpha());
				questionDto.setTitle(question.getTitle());
				questionDto.setTitleAlpha(question.getTitleAlpha());
				questionDtoList.add(questionDto);
			}
			ResponseDto responseDto = new ResponseDto(); //3
			responseDto.setQuestionDtoList(questionDtoList);
			return AmfResponse.to(responseDto);//5
		}
		ResponseDto responseDto = new ResponseDto(); //6
		responseDto.setQuestionDtoList(new ArrayList<QuestionDto>());
		return AmfResponse.to(responseDto);
	}
  1. BigTableから問題の全件を取得して、
  2. 問題用のDTOに詰め替えて、
  3. AMF返却用のDTOにぶちこんで
  4. AmfResponse.to(responseDto)でNavigationを返します。
  5. ここは気にしないでください。直す予定です。

ちなみにRequestDtoはこれからいろいろするためにもらってます。

で、BigTableから取得するとことか、このQuestionServiceはGuiceからDIされてますとか、全件返しちゃまずくないとか、6のとこ何よとかは置いといて、AMF返却用のQuestionDtoのソースです。

package jp.stk.gae.dto.question;


public class QuestionDto {
	/** title */
	private String title;

	/** title(ローマ字) */
	private String titleAlpha;

	/** 説明 */
	private String explain;

	/** 説明(ローマ字) */
	private String explainAlpha;

	/**
	 * titleを取得します。
	 * @return title
	 */
	public String getTitle() {
	    return title;
	}

	/**
	 * titleを設定します。
	 * @param title title
	 */
	public void setTitle(String title) {
	    this.title = title;
	}

	/**
	 * title(ローマ字)を取得します。
	 * @return title(ローマ字)
	 */
	public String getTitleAlpha() {
	    return titleAlpha;
	}

	/**
	 * title(ローマ字)を設定します。
	 * @param titleAlpha title(ローマ字)
	 */
	public void setTitleAlpha(String titleAlpha) {
	    this.titleAlpha = titleAlpha;
	}

	/**
	 * 説明を取得します。
	 * @return 説明
	 */
	public String getExplain() {
	    return explain;
	}

	/**
	 * 説明を設定します。
	 * @param explain 説明
	 */
	public void setExplain(String explain) {
	    this.explain = explain;
	}

	/**
	 * 説明(ローマ字)を取得します。
	 * @return 説明(ローマ字)
	 */
	public String getExplainAlpha() {
	    return explainAlpha;
	}

	/**
	 * 説明(ローマ字)を設定します。
	 * @param explainAlpha 説明(ローマ字)
	 */
	public void setExplainAlpha(String explainAlpha) {
	    this.explainAlpha = explainAlpha;
	}
}

ローマ字がAlphaって何よって思うかもですが、ローマ字を英語にするとなんていうのか知りません。
で次にAMF返却用のDTOです。

package jp.stk.gae.dto.question;

import java.util.List;

public class ResponseDto {
	/** 設問リスト */
	private List<QuestionDto> questionDtoList;

	/**
	 * 設問リストを取得します。
	 * @return 設問リスト
	 */
	public List<QuestionDto> getQuestionDtoList() {
	    return questionDtoList;
	}

	/**
	 * 設問リストを設定します。
	 * @param questionDtoList 設問リスト
	 */
	public void setQuestionDtoList(List<QuestionDto> questionDtoList) {
	    this.questionDtoList = questionDtoList;
	}
}

で最後に、GAEではBlazeDSがそのままだとうまいこと機能しないらしいので、T2AMFを利用して、通信をするための設定を行います。
web.xmlのT2Filterにパラメータを追加します。

	<filter>
		<filter-name>t2</filter-name>
		<filter-class>org.t2framework.t2.filter.T2Filter</filter-class>
		<init-param>
			<param-name>t2.rootpackage</param-name>
			<param-value>jp.stk.gae.page</param-value>
		</init-param>
 		<init-param>
			<param-name>t2.container.adapter</param-name>
			<param-value>org.t2framework.t2.adapter.GuiceAdapter</param-value>
		</init-param>
 		<init-param>
			<param-name>t2.exclude-resources</param-name>
			<param-value>css, js, png, gif, jpg</param-value>
		</init-param>
		<init-param>
                        <!-- これを入れる -->
			<param-name>t2.amf</param-name>
			<param-value>t2amf</param-value>
		</init-param>
	</filter>

ちなみにT2はMavenで取得すると、BlazeDsがもれなくついてきます。(Dependencyをもってるので。。。)なので、t2amfを使うなら消してもOKだと思われます。

つぎにactionScript側のソースです。
まず通信部分。

		private function setQuestionList():void{
			if(ro == null){
				ro = new RemoteObject("通信先");
				var endPoint:String = URLUtil.getFullURL("通信先URL" ,"t2.amf");
				var channel:Channel = URLUtil.isHttpsURL(endPoint)?
				new SecureAMFChannel(null,endPoint):new AMFChannel(null,endPoint);
				var channelSet:ChannelSet = new ChannelSet();
				channelSet.addChannel(channel);
				ro.channelSet = channelSet;
			}
			var token:AsyncToken = ro.putQuestionList(new RequestDto());
			token.addResponder(new AsyncResponder(handleResult,handleFault));
			questionList = new ArrayCollection();
		}

		public function handleResult(e:ResultEvent,token:Object=null):void{
			var responseDto:ResponseDto = ResponseDto(e.result);
			questionList = new ArrayCollection(responseDto.questionDtoList);//※
			gameInfo.status = GameInfo.STATUS_ALREADY;
		}

		public function handleFault(e:FaultEvent,token:Object=null):void{
			trace(ObjectUtil.toString(e));
		}

ソースはt2-haikuを参考にしました。

で次に、AMF受け取り用のQuestionDtoです。
※が気になった方もいるかも知れませんが、下で書きかます

/**
 * @author Administrator
 */
package jp.stk.gae.dto.question {
	[RemoteClass(alias="jp.stk.gae.dto.question.QuestionDto")]
	[Bindable]
	public class QuestionDto {
		private var _title:String;
		public function get title():String{return this._title}
		public function set title(title:String):void{this._title = title;}
		private var _titleAlpha:String;
		public function get titleAlpha():String{return this._titleAlpha}
		public function set titleAlpha(titleAlpa:String):void{this._titleAlpha = titleAlpa;}
		private var _explain:String;
		public function get explain():String{return this._explain;}
		public function set explain(explain:String):void {this._explain = explain;}
		private var _explainAlpha:String;
		public function get explainAlpha():String{return this._explainAlpha;}
		public function set explainAlpha(explainAlpha:String):void {this._explainAlpha = explainAlpha;}
		public function QuestionDto() {
		}
	}

}

次に、AMF受け取り用のResponseDtoです。

package jp.stk.gae.dto.question {
	[RemoteClass(alias="jp.stk.gae.dto.question.ResponseDto")]
	public class ResponseDto {
		public var questionDtoList:Array;//※
		public function ResponseDto() {

		}
	}

}

上の※もふくめて
BlazeDSを使ったことがある方は「あれ?」と思ったかもしれません。
ここがもっともはまったところです。

ResponseDtoにてJava側ではjava.util.Listで宣言しているのに、ActionScript側では、Arrayで宣言しています。普通(BlazeDS)はJavaでListを宣言した場合、ActionScript側はArrayCollectionかArrayListなのですが、t2Amfでは、ListはArrayとして返すようです。
このあたりはどこのサイトにも乗ってなく、
しぬほどハマりました。
(ちなみにBlazeDSをT2で利用する場合はArrayCollectionで返ります)
これでAMF通信ができました。

T2でAjax

今度はT2でAjaxをやってみます。
何をやるかというと、タイピングソフトなので、問題を作成するとき、日本語と、ローマ字を入力しなければならないのですが、その作業がめちゃくちゃめんどくさいのです。
なので、ボタンを押したらハイローマ字みたいな処理がほしかったのでそれをAjaxで作成します。

まず、Java側Pageクラスです。

	@Ajax
	@POST
	public Navigation tranceFormLatin(final WebContext context,
			@RequestParam("text")
			final String text) {
		String full = Transliterator.getInstance("Halfwidth-Fullwidth")
				.transliterate(furiganaService.getFurigana(text));
		String hira = Transliterator.getInstance("Katakana-Hiragana")
				.transliterate(full);
		String latin = Transliterator.getInstance("Hiragana-Latin")
				.transliterate(hira);
		String result = Transliterator.getInstance("Fullwidth-Halfwidth")
				.transliterate(
						latin
						.replace(" ", " ")
						.replace("、", ",")
						.replace("。",".")
						.replace("・", ",")
						.replace("~", "l")
				);
		return Json.convert(result);
	}

このリクエストパラメータを引数で取れるところがT2の相当素敵なところだと思います。
sastrutsで開発していた際、一つのActionが大きくなりすぎて、どのExecuteメソッドで、どのActionFormの値を使っているのか、わけがわからなくなることが多かったのです。
ActionFormがプロパティとして宣言されているため、実装時に気をつけないとよく起きていました。
ちなみに文章⇒振り仮名の返還はYahoo!のルビ振りサービスを利用して、UrlFetchでxmlを受け取って、解析し、作成しています。
振り仮名⇒ローマ字の返還はICU4Jを利用しています。

次に通信javascriptです。
jqueryを使ってます。

	$(function(){
		$("[name*=transeform]").click(function(){
			var id = $(this).attr("name").replace("transeform" , "");
			var text = $("#" + id).val();
			transeFormLatin(id , text);
		});
	});
	function transeFormLatin(id , text){
		$.ajax(
			{
				url     : "${t:url('通信先')}",
				type    : "post",
				dataType  : "json",
				data    : {
					"text" : text
				},
				success : function(response , json) {
					$("#" +id + "Alpha").val(response);
				},
				error   : function(xmlHttpReq, status, e) {
					$("#" +id + "Alpha").val("error");
				}
			}
		);
		return false;
	}

であとはボタンを押せばOKって感じです。

もうすこし作りこんだら、サイトの方を公開したいと思います。
しょぼしょぼですが。。。

T2が楽しそう

seasar connには行けなかったのですが、セッションの資料を見てると、T2が非常に面白そう。
今の時代にあった仕様のフレームワークですね

シルバーウィークを使ってT2+GAE+guice+flex+mavenでアプリを作ろうと思います。

てか前に作ってたタイピングアプリのサーバー側をこれにします。

1.プロジェクト作成

まずEclipseのプロジェクトを作成します。
プロジェクトは、T2のプロジェクトサイトにあったViliを使います。

q4e等でMavenプロジェクトとして作らないのは、
あとからGAEのプロジェクト構成にするのが意外とめんどくさかったからです。
(appengineのjarが1.2.5のものがセントラルリポジトリにまだなかったからでもあります)

Viliをウィザードに従い、「T2プロジェクト for GAE/J」を選択します。

プロジェクト名、ルートパッケージ名(T2でのpageパッケージの親に当たるところ?)、バージョンを設定します。

あとは文字エンコードを「UTF-8」にして、作成します。

すると以下のようなGAE+T2のプロジェクトが作成できます。

2.Mavenの設定

つぎはMavenの設定をするため、pom.xmlを作成します。
ただその前に個人的に今までsrcフォルダの直下に
パッケージを置いたことがなかったので何か見た目が気になります。
なのでMaven標準?のsrc/main/java、src/main/resources、
src/test/java、src/test/resourcesを作成し設定します。
このときもともとあったlog4j.properties、
META-INFはsrc/main/resourcesに移動しておきます。

で次にpom.xmlを作成します。

<project xmlns="http://maven.apache.org/POM/4.0.0"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<groupId>jp.stk.gae</groupId>
	<artifactId>soundtrick</artifactId>
	<packaging>war</packaging>
	<version>0.0.1-SNAPSHOT</version>
	<name>soundtrick</name>
	<dependencies>
		<dependency>
			<groupId>junit</groupId>
			<artifactId>junit</artifactId>
			<version>3.8.1</version>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.t2framework.web</groupId>
			<artifactId>t2</artifactId>
			<version>0.6.1-ga</version>
		</dependency>
		<dependency>
			<groupId>org.t2framework.web</groupId>
			<artifactId>guiceadapter</artifactId>
			<version>0.6.1-ga</version>
		</dependency>
		<dependency>
			<groupId>com.google.code.guice</groupId>
			<artifactId>guice-servlet</artifactId>
			<version>1.0</version>
		</dependency>
		<dependency>
			<groupId>taglibs</groupId>
			<artifactId>standard</artifactId>
			<version>1.1.2</version>
		</dependency>
		<!--
			<dependency>
			<groupId>org.apache.geronimo.specs</groupId>
			<artifactId>geronimo-jta_1.1_spec</artifactId>
			<version>1.0</version>
			</dependency>
			<dependency>
			<groupId>org.apache.geronimo.specs</groupId>
			<artifactId>geronimo-jpa_3.0_spec</artifactId>
			<version>1.0</version>
			</dependency>
		-->
	</dependencies>
	<build>
		<defaultGoal>install</defaultGoal>
		<outputDirectory>war/WEB-INF/classes</outputDirectory>
		<testOutputDirectory>target/test-classes</testOutputDirectory>
		<sourceDirectory>${basedir}/src/main/java</sourceDirectory>
		<testSourceDirectory>
			${basedir}/src/test/java
		</testSourceDirectory>
		<scriptSourceDirectory>${basedir}/war</scriptSourceDirectory>
		<plugins>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-dependency-plugin</artifactId>
				<configuration>
					<outputDirectory>war/WEB-INF/lib</outputDirectory>
					<excludeScope>provided</excludeScope>
					<includeScope>compile</includeScope>
					<excludeGroupIds>
						javax.servlet,javax.transaction
					</excludeGroupIds>
					<overWriteReleases>false</overWriteReleases>
					<overWriteSnapshots>true</overWriteSnapshots>
					<overWriteIfNewer>true</overWriteIfNewer>
				</configuration>
				<executions>
					<execution>
						<phase>process-resources</phase>
						<goals>
							<goal>copy-dependencies</goal>
						</goals>
					</execution>
				</executions>
			</plugin>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-war-plugin</artifactId>
				<configuration>
					<warSourceDirectory>war</warSourceDirectory>
				</configuration>
			</plugin>
			<plugin>
				<!-- http://www.datanucleus.org/products/accessplatform/enhancer.html#maven2 -->
				<groupId>org.datanucleus</groupId>
				<artifactId>maven-datanucleus-plugin</artifactId>
				<version>1.1.0</version>
				<configuration>
					<verbose>true</verbose>
					<mappingIncludes>**/*.class</mappingIncludes>
					<fork>false</fork>
					<enhancerName>ASM</enhancerName>
					<api>JDO</api>
					<jdkLogConfiguration>
						war/WEB-INF/logging.properties
					</jdkLogConfiguration>
				</configuration>
				<executions>
					<execution>
						<phase>compile</phase>
						<goals>
							<goal>enhance</goal>
						</goals>
					</execution>
				</executions>
			</plugin>
		</plugins>
		<pluginManagement>
			<plugins>
				<plugin>
					<artifactId>maven-compiler-plugin</artifactId>
					<configuration>
						<encoding>UTF-8</encoding>
						<source>1.6</source>
						<target>1.6</target>
					</configuration>
				</plugin>
				<plugin>
					<artifactId>maven-war-plugin</artifactId>
					<version>2.0</version>
					<configuration>
						<warSourceDirectory>
							${basedir}/war
						</warSourceDirectory>
						<warSourceExcludes>
							.*,.*/,target/,work/,pom.xml,war/WEB-INF/classes/**/*.class,war/WEB-INF/lib/
						</warSourceExcludes>
						<!--
							<webResources>
							<resource>
							<directory>
							${basedir}/target/generated-resources
							</directory>
							</resource>
							</webResources>
						-->
						<archive>
							<manifest>
								<mainClass>
									org.t2framework.executablewar.Main
								</mainClass>
							</manifest>
						</archive>
					</configuration>
				</plugin>
			</plugins>
		</pluginManagement>
	</build>
	<repositories>
		<repository>
			<id>jpox</id>
			<url>http://www.jpox.org/downloads/maven2</url>
		</repository>
		<repository>
			<id>DataNucleus</id>
			<url>http://www.datanucleus.org/downloads/maven2</url>
		</repository>
		<repository>
			<id>DataNucleus_Nightly_Repos</id>
			<url>
				http://www.datanucleus.org/downloads/maven-nightly
			</url>
			<layout>legacy</layout>
		</repository>
		<repository>
			<id>mvnsearch</id>
			<url>http://www.mvnsearch.org/maven2</url>
		</repository>
		<repository>
			<id>maven.t2framework.org</id>
			<name>The T2 Project Maven2 Repository</name>
			<url>http://maven.t2framework.org/maven2</url>
		</repository>
		<repository>
			<id>maven-snapshot.t2framework.org</id>
			<url>http://maven.t2framework.org/maven2-snapshot</url>
			<snapshots>
				<enabled>true</enabled>
			</snapshots>
		</repository>
	</repositories>
	<distributionManagement>
		<repository>
			<uniqueVersion>false</uniqueVersion>
			<id>maven.t2framework.org</id>
			<name>The T2 Project Maven2 Repository</name>
			<url>dav:http://maven.t2framework.org/maven2</url>
		</repository>
		<snapshotRepository>
			<uniqueVersion>true</uniqueVersion>
			<id>maven.t2framework.org</id>
			<name>The T2 Project Maven2 Snapshot Repository</name>
			<url>dav:http://maven.t2framework.org/maven2-snapshot</url>
		</snapshotRepository>
	</distributionManagement>
	<pluginRepositories>
		<pluginRepository>
			<id>DataNucleus</id>
			<url>http://www.datanucleus.org/downloads/maven2</url>
		</pluginRepository>
	</pluginRepositories>
</project>

もともと現在リリースされているViliだとT2のVer0.6対応なので
その辺のバージョンもあげておきます。
細かいところが間違っていたらすいません。
大事なのは、maven-dependency-pluginの
outputDirectoryがwar/WEB-INF/libになっているらへんです。
こうしないとGAEのローカルサーバが見てくれません。
あとはmvn clean eclipse:eclipse dependency:copy-dependenciesコマンドで
ライブラリを落としてくればOKです。
ちなみにcleanゴールで実行すると、appengine周りのライブラリが消えるので、
プロジェクトを選択し、「プロパティ」→「Google」→「App Engine」から
「Use Google App Engine」を一度外し、再度つけてください。

3.Guiceの設定

次にGuiceの設定です。
Viliでプロジェクトを作成すると、web.xmlも自動で作成してくれるので、
これにGuiceの設定を入れていきます。

<?xml version="1.0"?>
<web-app id="t2-test" version="2.4"
	xmlns="http://java.sun.com/xml/ns/j2ee"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">
	<context-param>
		<param-name>t2.encoding</param-name>
		<param-value>UTF-8</param-value>
	</context-param>
	<filter>
		<filter-name>Guice Servlet Filter</filter-name>
		<filter-class>
			com.google.inject.servlet.GuiceFilter
		</filter-class>
	</filter>
	<filter>
		<filter-name>t2</filter-name>
		<filter-class>org.t2framework.t2.filter.T2Filter</filter-class>
		<init-param>
			<param-name>t2.rootpackage</param-name>
			<param-value>jp.stk.gae.page</param-value>
		</init-param>
		<init-param>
			<param-name>t2.container.adapter</param-name>
			<param-value>
				org.t2framework.t2.adapter.GuiceAdapter
			</param-value>
		</init-param>
		<init-param>
			<param-name>t2.exclude-resources</param-name>
			<param-value>css, js, png, gif, jpg</param-value>
		</init-param>
	</filter>
	<filter-mapping>
		<filter-name>Guice Servlet Filter</filter-name>
		<url-pattern>/*</url-pattern>
	</filter-mapping>
	<filter-mapping>
		<filter-name>t2</filter-name>
		<url-pattern>/*</url-pattern>
		<dispatcher>REQUEST</dispatcher>
		<dispatcher>FORWARD</dispatcher>
		<!--
			<dispatcher>INCLUDE</dispatcher>
			<dispatcher>ERROR</dispatcher>
		-->
	</filter-mapping>

	<welcome-file-list>
		<welcome-file>index.jsp</welcome-file>
	</welcome-file-list>

	<jsp-config>
		<jsp-property-group>
			<url-pattern>*.jsp</url-pattern>
			<el-ignored>false</el-ignored>
			<page-encoding>UTF-8</page-encoding>
			<scripting-invalid>false</scripting-invalid>
			<!-- <include-prelude>/WEB-INF/pages/common.jsp</include-prelude> -->
			<!-- trim white spaces (JSP 2.1 or higher)
				<trim-directive-whitespaces>true</trim-directive-whitespaces>
			-->
		</jsp-property-group>
	</jsp-config>

</web-app>

T2Filterのt2.container.adapterにorg.t2framework.t2.adapter.GuiceAdapterを設定し、
T2Filterの前にGuiceFilterを設定します。

4.GuiceModuleの設定

META-INFにservices/com.google.inject.Moduleを作成し、
読み込むModuleを作成します。
ここではT2のGuiceサンプルにあった、ApplicationModule.javaを作成します。

package jp.stk.gae.module;


import jp.stk.gae.plugin.PagePluginImpl;

import org.t2framework.t2.plugin.Plugin;

import com.google.inject.AbstractModule;
import com.google.inject.servlet.ServletModule;

public class ApplicationModule extends AbstractModule {

	@Override
	protected void configure() {
		install(new ServletModule());
	}
}

そしてこのModuleを読み込んでもらうために、
com.google.inject.ModuleにこのModuleのフル
クラスパスを書きます。(jp.stk.gae.module.ApplicationModule )

とりあえず今日はここまでです。
あとはT2Pluginを設定したり、AOP設定したり、いろいろやってみます。

AIR GEARでタイピングアプリを作り中

Swingの作成をしながら、そろそろAIRにも手を出すべきと思って、タイピングアプリを作ってます。
最終的にはGAE/Jからデータ引っ張ってきてほげほげしたいです。

もともとJavaメインで今まで来ましたが、SwingがJavaから切られるんでないかガタブルしていて、
JavaFxには余り期待ができないので、ここはAIRでしょとおもい作成を開始しました。

そこで問題になるのが、IDE
いまさらメモ帳ではやる気がしません。

FlashDevelopも試したのですが、GUIアプリを作るのに、
GUIエディタがないと発狂しそうになります。

そこで素敵なのが、AIR GEARです。

たぶん唯一OSSで利用できるAIRGUIエディタなのではないでしょうか。

タイピングアプリができたら記事にしたいと思います。

WebDriver

Javaでブラウザを操作するライブラリ WebDriverをためしで使ってみました。

http://journal.mycom.co.jp/articles/2009/05/26/webdriver/index.html

使い心地は、かなりよいです。
DOM Elementの取得にXPathが使えるところはとても気に入っています。

ただXPathでElementをとりにいったときに、見つからない場合、Exceptionをスローするのですが、個人的にはnullで返してくれたほうがメンドクサクナクテなくてよかったです。

もともとこの手のブラウザ操作系のアプリはVBA(Excelの)でInternetExplorerObjectを使ってやっていたのですが、
それに比べてもさまざまな条件で扱いやすいです。

あえてひとつわがままなことを言えば、すでに開いているブラウザを操作できるとなおすてきなのですが。。。


まあ無理ですよね。。。
(もしかしてWindowHandle取得したらできる?swichToとか?)