Kotlin で始める JavaEE 7 〜 JPA + CDI + JSF 〜

f:id:Naotsugu:20150502164325p:plain

blog1.mammb.com に引き続き、 blog1.mammb.com をKotlin化してみます。


設定ファイルなどは同じものを利用するので必要に応じてこちらを参照してください。

Resources.kt

CDIの Produces を提供する Resources です。

package example;

import javax.enterprise.context.RequestScoped;
import javax.enterprise.inject.Produces;
import javax.faces.context.FacesContext;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;

public open class Resources {

    [Produces]
    [PersistenceContext]
    open var em : EntityManager? = null
    
    [Produces]
    [RequestScoped]
    public open fun produceFacesContext() : FacesContext?  {
        return FacesContext.getCurrentInstance()
    }
}

Kotlin では C# 風にアノテーションを[]で定義します。 Kotlin の将来のリリースでは Java のように @ を利用するように変更される予定となっています。

Kotlin では null セーフな変数を扱うため、null となる可能性があるものは型定義の後ろに ? を付ける必要があります。

Member.kt

Member エンティティです。name と email フィールドと id フィールドを定義します。

package example.model

import java.io.Serializable
import javax.persistence.Entity
import javax.persistence.GeneratedValue
import javax.persistence.Id
import javax.validation.constraints.NotNull
import javax.validation.constraints.Pattern
import javax.validation.constraints.Size

[Entity]
public class Member(
    [Id] [GeneratedValue]
    var id : Long = 0,
    [NotNull] [Size(min = 1, max = 25)] [Pattern(regexp = "[^0-9]*", message = "Must not contain numbers")]
    var name : String = "",
    [NotNull] [Pattern(regexp = "^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\\.[a-zA-Z0-9-]+)*$", message = "not email")]
    var email : String = ""
) : Serializable { }

getter/setter はクラスへのコンパイル時に生成されるため、フィールド定義だけしておけば大丈夫です。

なお、フィールド値は JPA で扱うため var で宣言します。

MemberRepository.kt

リポジトリです。Member を id で取得するメソッドと、email で取得するメソッド。一覧を取得するメソッドを定義します。

package example.data

import example.model.Member
import javax.enterprise.context.ApplicationScoped
import javax.inject.Inject
import javax.persistence.EntityManager
import javax.persistence.criteria.Expression

[ApplicationScoped]
public open class MemberRepository {

    [Inject]
    open var em : EntityManager? = null

    public open fun findById(id : Long) : Member = em!!.find(javaClass<Member>(), id)

    public open fun findByEmail(email : String):Member {
        val cb = em!!.getCriteriaBuilder()
        val criteria = cb.createQuery(javaClass<Member>())
        val member = criteria.from(javaClass<Member>())
        val emailExp : Expression<String> = member.get("email")
        criteria.select(member).where(cb.equal(emailExp, email))
        return em!!.createQuery(criteria).getSingleResult()
    }

    public open fun findAllOrderedByName():List<Member>  {
        val cb = em!!.getCriteriaBuilder()
        val criteria = cb.createQuery(javaClass<Member>())
        val member = criteria.from(javaClass<Member>())
        val nameExp : Expression<String> = member.get("name")
        criteria.select(member).orderBy(cb.asc(nameExp))
        return em!!.createQuery(criteria).getResultList()
    }
}

ApplicationScoped アノテーションにてCDI管理のスコープを指定しています。

クラスは open を付けています。open 指定が無い場合、final となり CDI がプロキシを生成できなくなるためです。

Java で cb.createQuery(Member.class) このようなクラス指定は cb.createQuery(javaClass<Member>()) このように書きます。

MemberRegistration.kt Member の登録用のステートレスセッションビーンです。

package example.service

import example.model.Member
import javax.ejb.Stateless
import javax.enterprise.event.Event
import javax.inject.Inject
import javax.persistence.EntityManager

[Stateless]
public open class MemberRegistration {
    
    [Inject]
    var em : EntityManager? = null

    [Inject]
    var memberEventSrc :Event<Member>? = null

    open fun register(member:Member?) {
        em!!.persist(member)
        memberEventSrc!!.fire(member)
    }
}

こちらも open 指定します。

MemberListProducer.kt

Member の一覧を供給する CDI イネーブルドビーンです。

package example.data

import example.data.MemberRepository
import example.model.Member
import java.util.ArrayList
import javax.annotation.PostConstruct
import javax.enterprise.context.RequestScoped
import javax.enterprise.event.Observes
import javax.enterprise.event.Reception
import javax.enterprise.inject.Produces
import javax.inject.Inject
import javax.inject.Named

[RequestScoped]
public open class MemberListProducer {
    [Inject]
    open var memberRepository : MemberRepository? = null
    
    var memberList : List<Member> = ArrayList<Member>()

    [Produces] [Named]
    open fun getMembers():List<Member> = memberList

    public open fun onMemberListChanged([Observes(notifyObserver = Reception.IF_EXISTS)] member :Member) {
        retrieveAllMembersOrderedByName();
    }

    [PostConstruct]
    public open fun retrieveAllMembersOrderedByName() {
        memberList = memberRepository!!.findAllOrderedByName();
    }   
}

MemberController.kt

jSF のバッキングビーンにです。

package example.controller

import example.model.Member
import example.service.MemberRegistration
import javax.annotation.PostConstruct
import javax.enterprise.inject.Model
import javax.enterprise.inject.Produces
import javax.faces.application.FacesMessage
import javax.faces.context.FacesContext
import javax.inject.Inject
import javax.inject.Named

[Model]
public open class MemberController {
    [Inject]
    open var facesContext : FacesContext? = null

    [Inject]
    open var memberRegistration : MemberRegistration? = null

    [Produces] [Named]
    open var newMember : Member? = null

    [PostConstruct]
    public open fun initNewMember() {
        newMember = Member()
    }

    public open fun register() {
        try {
            memberRegistration!!.register(newMember)
            facesContext!!.addMessage(null, FacesMessage(FacesMessage.SEVERITY_INFO, "Registered!", "Registration successful"))
            initNewMember()
        } catch (e : Exception) {
            val errorMessage = "Registration failed. See server log for more information"
            val m = FacesMessage(FacesMessage.SEVERITY_ERROR, errorMessage, "Registration unsuccessful")
            facesContext!!.addMessage(null, m)
        }
    }
}

build.gradle

kotlin-gradle-plugin を使います。

buildscript {
    repositories {
        mavenCentral()
        jcenter()
    }
    dependencies {
        classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:0.11.91.4'
        classpath 'com.bmuschko:gradle-cargo-plugin:2.1'
    }
}

apply plugin: 'java'
apply plugin: 'kotlin'
apply plugin: 'war'
apply plugin: 'idea'
apply plugin: 'com.bmuschko.cargo'

sourceCompatibility = 1.8
targetCompatibility = 1.8


[compileJava, compileTestJava]*.options*.encoding = 'UTF-8'

repositories {
  mavenCentral()
}

dependencies {
  compile 'org.jetbrains.kotlin:kotlin-stdlib:0.11.91.4'
  providedCompile 'org.jboss.spec:jboss-javaee-all-7.0:1.0.2.Final'
  testCompile 'junit:junit:4.11'
}


cargo {
    containerId = 'wildfly8x'
    deployable {
        file = file(war.archivePath)
    }
    local {
        installer {
            installUrl = 'http://download.jboss.org/wildfly/8.2.0.Final/wildfly-8.2.0.Final.zip'
            downloadDir = new File(buildDir, "download")
            extractDir = file("wildfly")
        }
        containerProperties { property 'cargo.jboss.configuration', 'standalone-full' }
    }
}

実行

./gradlew war cargoRunLocal -i

起動したら以下をブラウザで開きます。

f:id:Naotsugu:20150514231307p:plain

あまり Kotlin っぽいコードは出てきませんでしたが、簡単に動きます。