ユーザサービスクラスを作成する
ユーザに紐づく処理を実装する UserService クラスを作ってみます。
とりあえず、パッケージは homework.service で。
エンティティマネジャーの取得
JPAではエンティティマネジャーによって、いろいろな操作を実行します。
CDIを利用してアノテーションの指定でDI出来たりするみたいですが、それはまた別の機会に勉強することにして、ファクトリクラスからインスタンスを取得するようにしたいと思います。
本番用と単体テスト用の切り替えは環境変数で指定するという仕様として、以下の EMProducer クラスを作成しました。
package homework.utils;
import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;
import org.apache.commons.lang3.StringUtils;
/**
* エンティティマネジャー生成クラス。
* @author satake
*/
public final class EMProducer {
/** 環境変数からパーシスタントユニット名を取得するキーの定義 */
private static final String KEY_UNIT_NAME = "jpaUnitName";
/** 通常のパーシスタントユニット名の定義 */
private static final String DEFAULT_UNIT_NAME = "homework";
/** 自インスタンス */
private static EMProducer producer = null;
/** エンティティマネジャーファクトリー */
private final EntityManagerFactory emFactory;
/**
* 非インスタンス化のための private コンストラクタ。
*/
private EMProducer() {
String envUnitName = System.getProperty(KEY_UNIT_NAME);
String unitName = StringUtils.isNotEmpty(envUnitName) ? envUnitName : DEFAULT_UNIT_NAME;
emFactory = Persistence.createEntityManagerFactory(unitName);
}
/**
* 自インスタンスを取得する。
* @return 自インスタンス
*/
private static EMProducer getInstance() {
if (producer == null) {
producer = new EMProducer();
}
return producer;
}
/**
* エンティティマネジャーを生成する。
* @return エンティティマネジャー
*/
public static EntityManager createManager() {
return getInstance().emFactory.createEntityManager();
}
}
ログイン処理
ログイン処理では、アカウントIDとパスワードでユーザ認証を行い、ユーザが存在していたら直前の結果から間違えた問題の一覧を取得します。
認証処理ではアカウントIDとパスワードが検索の条件となるので、これを条件としたJPQLを homework.xml に記載します。
<!-- アカウントIDとパスワードでユーザ情報を取得する -->
<named-query name="User.findByAccountPasswd">
<query><![CDATA[
SELECT
u
FROM
User u
WHERE
u.accountId = :accountId
AND
u.password = :password
]]></query>
</named-query>
JPAではエンティティオブジェクトをベースに記述します。
また、オブジェクトには別名をつけ、1テーブルのアクセスでも「別名.変数名」と別名経由で指定する必要があります。
上記のJPQLを利用した authenticate メソッドは以下の通りになります。
private User authenticate(String accountId, String password) {
try {
return entityManager
.createNamedQuery("User.findByAccountPasswd", User.class)
.setParameter("accountId", accountId)
.setParameter("password", Crypter.encrypt(password))
.getSingleResult();
}
catch (NoResultException e) {
logger.warn(e);
return null;
}
}
- entityManager はコンストラクタで EMProducer.createManager() で取得したインスタンス変数です。
- 次に定義したJPQLを呼び出すために createNamedQuery メソッドを実行します。このとき第2引数は結果となるオブジェクトのクラスを指定します。
- 次にパラメータを setParameter メソッドで指定していきます。
- 最後にここでは単一の結果を返すので getSingleResult メソッドを実行します。
- なお getSingleResult で結果が取得できない場合は NoResultException が throw されます。
ちなみに Crypter.encrypt は作成した暗号化のユーティリティクラスです。
Blowfish で暗号化したものを Base64 エンコードしてます。
一応コードは以下の通りです。
package homework.utils;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import javax.crypto.Cipher;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.codec.binary.Base64;
/**
* 暗号ユーティリティ。
* <p>
* 暗号化・復号化の static メソッドを提供するユーティリティクラス。
* </p>
* @author satake
*/
public final class Crypter {
/** 秘密鍵の定義 */
private static final String SECRET_KEY = "homework";
/** 暗号化・復号化のアルゴリズムの定義 */
private static final String ALGORITHM = "Blowfish";
/**
* 非インスタンス化のためのprivateコンストラクター。
*/
private Crypter() {
}
/**
* 文字列を暗号化する。
* @param target 暗号化する文字列
* @return 暗号化した文字列
*/
public static String encrypt(String target) {
try {
Cipher cipher = createCipher(Cipher.ENCRYPT_MODE);
byte[] encrypted = cipher.doFinal(target.getBytes());
return (new Base64()).encodeToString(encrypted);
}
catch (Exception e) {
throw new SystemException("暗号化に失敗しました。", e);
}
}
/**
* 文字列を復号化する。
* @param target 復号化する文字列
* @return 復号化した文字列
*/
public static String decrypt(String target) {
try {
byte[] encrypted = (new Base64()).decode(target);
Cipher cipher = createCipher(Cipher.DECRYPT_MODE);
byte[] decrypted = cipher.doFinal(encrypted);
return new String(decrypted, "UTF-8");
}
catch (Exception e) {
throw new SystemException("復号化に失敗しました。", e);
}
}
/**
* 暗号機能オブジェクトを生成する。
* @param mode 暗号化する場合は {@link Cipher#ENCRYPT_MODE} を、
* 復号化する場合は {@link Cipher#DECRYPT_MODE} を指定する。
* @return 暗号機能オブジェクトのインスタンス。
* @throws NoSuchAlgorithmException アルゴリズムが使用できない場合。
* @throws NoSuchPaddingException パディングが使用できない場合。
* @throws InvalidKeyException キーの指定が無効な場合。
*/
private static Cipher createCipher(int mode) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException {
SecretKeySpec secretKeySpec = new SecretKeySpec(SECRET_KEY.getBytes(), ALGORITHM);
Cipher cipher = Cipher.getInstance(ALGORITHM);
cipher.init(mode, secretKeySpec);
return cipher;
}
}
次に直前の結果から間違えた問題の一覧を取得する処理ですが、ユーザに紐づく最新の履歴情報にある回答を取得した後不正解のもののみを取得するという仕様で作ってみます。
<!-- 直近の回答履歴情報を取得する -->
<named-query name="Answer.findLatestHistory">
<query><![CDATA[
SELECT
a
FROM
Answer a
WHERE
a.history.historyId =
(
SELECT
max(h.historyId)
FROM
History h
WHERE
h.user.userId = :userId
)
ORDER BY
a.answerId
]]></query>
</named-query>
private List<Answer> getLatestWrongAnswers(User user) {
List<Answer> answers = entityManager
.createNamedQuery("Answer.findLatestHistory", Answer.class)
.setParameter("userId", user.getUserId())
.getResultList();
return answers.stream()
.filter(answer -> StringUtils.equals(answer.getCorrectWrong(), CorrectWrong.WRONG))
.collect(Collectors.toList());
}
- getResultList で値を取得した後、Java8 の Stream API で不正解のもののみをフィルターします。
認証処理と誤答問題取得処理を組み合わせたログインメソッドは以下のようになります。
public Responce login(User auth) {
Responce responce = new Responce();
User user = authenticate(auth.getAccountId(), auth.getPassword());
responce.setUser(user);
if (user != null) {
responce.setAnswers(getLatestWrongAnswers(user));
}
return responce;
}
とりあえず、DBへのアクセス処理を書いたので、次はテストを実行します。