/*
* Copyright 2012 Kazumune Katagiri. (http://d.hatena.ne.jp/nemuzuka)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
* either express or implied. See the License for the specific language
* governing permissions and limitations under the License.
*/
package jp.co.nemuzuka.core.controller;
import java.lang.reflect.Method;
import java.util.ConcurrentModificationException;
import java.util.logging.Level;
import jp.co.nemuzuka.core.annotation.NoRegistCheck;
import jp.co.nemuzuka.core.annotation.Validation;
import jp.co.nemuzuka.core.entity.UserInfo;
import jp.co.nemuzuka.exception.AlreadyExistKeyException;
import jp.co.nemuzuka.utils.DateTimeChecker;
import org.slim3.controller.Navigation;
import org.slim3.controller.validator.Validators;
import com.google.appengine.api.users.UserService;
import com.google.appengine.api.users.UserServiceFactory;
/**
* Htmlを返却するControllerの基底クラス.
* 基本的に、これを継承したControllerからデータストアのアクセスは考えていません。
* @author kazumune
*/
public abstract class HtmlController extends AbsController {
/**
* メイン処理.
* @return 遷移先Navigation
* @throws Exception 例外
*/
abstract protected Navigation execute() throws Exception;
/**
* メイン処理.
* 正常終了時、commitしてThreadLocalから削除します。
* ConcurrentModificationExceptionやAlreadyExistKeyExceptionは
* 本クラスを継承したクラスでは発生しない設計思想なので、
* エラー画面に遷移させます。
* ※更新は、Ajax側で行う
* @see org.slim3.controller.Controller#run()
*/
@Override
protected Navigation run() throws Exception {
//グローバルトランザクションの設定を行う
setTransaction();
Navigation navigation = null;
try {
//UserInfoの確認
checkAndSetUserInfo(getClass());
navigation = execute();
//commit
executeCommit();
} catch (ConcurrentModificationException e) {
//今回の思想では、こちらのケースで排他エラーになるような処理は無いので、
//強制的にエラー画面を表示させることとする
//ここに来たら設計バグ
super.tearDown();
navigation = forward(ERR_URL_SYSERROR);
} catch(AlreadyExistKeyException e) {
//一意制約エラーの場合
//ここに来たら設計バグ
super.tearDown();
navigation = forward(ERR_URL_SYSERROR);
} catch(Exception e) {
logger.log(Level.SEVERE, e.getMessage(), e);
throw e;
}
return navigation;
}
/**
* 前処理.
* ・ActionFormの設定
* ・validation
* を行います。
* @see org.slim3.controller.Controller#setUp()
*/
@SuppressWarnings("rawtypes")
@Override
protected Navigation setUp() {
super.setUp();
Class clazz = getClass();
setUserService();
boolean sessionCheck = executeSessionCheck(clazz);
if(sessionCheck == false) {
//SessionTimeoutの場合、エラー画面に遷移
return forward(ERR_SESSION_TIMEOUT);
}
//ログインユーザの情報を元に、データストアに設定されているかチェック
Navigation navigation = checkSettingUser(clazz);
if(navigation != null) {
return navigation;
}
//ProjectAdmin、ProjectMember、SystemManagerの設定確認
boolean projectAdmin = executeProjectAdminCheck(clazz);
boolean projectMember = executeProjectMemberCheck(clazz);
boolean systemManager = executeSystemManagerCheck(clazz);
if(projectAdmin == false || projectMember == false || systemManager == false) {
//不正なエラーの場合、TOP画面に遷移させる
getUserInfo().initProjectInfo();
return forward("/");
}
//ActionFormの設定
setActionForm(clazz);
//validationの実行
return executeValidation(clazz);
}
/**
* ログインユーザ設定チェック.
* ログインユーザが登録済みである or Google App Engine管理者であるかチェックを行います。
* 「@NoRegistCheck」が付与されている場合、強制的にnullを返します。
* @param clazz 対象クラス
* @return 登録済みである or GAE管理者である場合、null/それ以外、強制遷移先Navigation
*/
@SuppressWarnings({ "rawtypes" })
private Navigation checkSettingUser(Class clazz) {
//executeメソッドにValidatetionアノテーションが付与されている場合
Method target = null;
try {
target = getDeclaredMethod(clazz, "execute", (Class[])null);
} catch (Exception e) {
throw new RuntimeException(e);
}
NoRegistCheck validation = target.getAnnotation(NoRegistCheck.class);
if(validation != null) {
return null;
}
UserService service = UserServiceFactory.getUserService();
if(service.isUserAdmin()) {
//管理者がログインした場合、処理終了
return null;
}
//登録済みユーザであることを確認する
if(isExistsUser(service.getCurrentUser().getEmail()) == false) {
//存在しないので遷移先のNavigation
return forward(ERR_URL_NO_REGIST);
}
return null;
}
/**
* validation実行.
* メイン処理に「@Validation」が付与されれている場合、メソッドを呼び出し、validateを実行します。
* @param clazz 対象クラス
* @return エラーが無い or validatationが無い場合はnull/エラーが存在する場合、遷移先Navigation
*/
@SuppressWarnings({ "rawtypes" })
private Navigation executeValidation(Class clazz) {
//executeメソッドにValidatetionアノテーションが付与されている場合
Method target = null;
try {
target = getDeclaredMethod(clazz, "execute", (Class[])null);
} catch (Exception e) {
throw new RuntimeException(e);
}
Validation validation = target.getAnnotation(Validation.class);
if(validation != null) {
Validators validators = (Validators)invoke(clazz, validation.method());
//validate実行
boolean bret = validators.validate();
//エラーが存在する場合
if(bret == false) {
//inputのメソッドで呼び出された定義を呼び出すようにする
return (Navigation) invoke(getClass(), validation.input());
}
}
return null;
}
/**
* UserInfo確認.
* Sessionが存在しない or UserInfoの更新開始時刻を現在の時刻が超えた(もしくはnull)
* の場合、参照可能プロジェクトを更新と共に更新開始時期を
* 現在時刻 + システムプロパティ(jp.co.nemuzuka.session.refresh.min)分加算して設定する
* 「@NoRegistCheck」が付与されている場合、チェックを行いません。
* @param clazz 対象クラス
*/
@SuppressWarnings({ "rawtypes" })
private void checkAndSetUserInfo(Class clazz) {
//executeメソッドにValidatetionアノテーションが付与されている場合、処理終了
Method target = null;
try {
target = getDeclaredMethod(clazz, "execute", (Class[])null);
} catch (Exception e) {
throw new RuntimeException(e);
}
NoRegistCheck validation = target.getAnnotation(NoRegistCheck.class);
if(validation != null) {
return;
}
UserInfo userInfo = sessionScope(USER_INFO_KEY);
if(userInfo == null || DateTimeChecker.isOverRefreshStartTime(userInfo.refreshStartTime)) {
//更新する
if(userInfo == null) {
userInfo = new UserInfo();
}
refreshUserInfo(userInfo);
sessionScope(USER_INFO_KEY, userInfo);
}
}
}