読者です 読者をやめる 読者になる 読者になる

分速で始める JavaEE 7 〜 JPA + CDI + JAX-RS 〜

Java JavaEE

f:id:Naotsugu:20150213230909p:plain

前回 分速で始める JavaEE 7 〜 JPA + CDI + JSF 〜 - A Memorandum の続き。

RESTサービス追加します。

ファイル準備

パッケージ用のディレクトリ追加とファイル作成します。

mkdir -p src/main/java/example/rest
touch src/main/java/example/rest/JaxRsActivator.java
touch src/main/java/example/rest/MemberResourceService.java

JaxRsActivator.java

Application を継承したクラスを作成します。

package example.rest;

import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;

@ApplicationPath("/rest")
public class JaxRsActivator extends Application {
}

@ApplicationPath で指定したパスがRESTサービスのルートURLになります。

MemberResourceService.java

JAX-RS のサービスクラスを作成します。少し長いですが大したことしてません。

package example.rest;

import example.data.MemberRepository;
import example.model.Member;
import example.service.MemberRegistration;

import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.persistence.NoResultException;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import javax.validation.ValidationException;
import javax.validation.Validator;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

@Path("/members")
@RequestScoped
public class MemberResourceService {

    @Inject
    private Validator validator;

    @Inject
    private MemberRepository repository;

    @Inject
    MemberRegistration registration;

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public List<Member> listAllMembers() {
        return repository.findAllOrderedByName();
    }

    @GET
    @Path("/{id:[0-9][0-9]*}")
    @Produces(MediaType.APPLICATION_JSON)
    public Member lookupMemberById(@PathParam("id") long id) {
        Member member = repository.findById(id);
        if (member == null) {
            throw new WebApplicationException(Response.Status.NOT_FOUND);
        }
        return member;
    }

    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Response createMember(Member member) {

        Response.ResponseBuilder builder = null;

        try {
            validateMember(member);
            registration.register(member);
            builder = Response.ok();

        } catch (ConstraintViolationException ce) {
            // Handle bean validation issues
            builder = createViolationResponse(ce.getConstraintViolations());
        } catch (ValidationException e) {
            // Handle the unique constrain violation
            Map<String, String> responseObj = new HashMap<>();
            responseObj.put("email", "Email taken");
            builder = Response.status(Response.Status.CONFLICT).entity(responseObj);
        } catch (Exception e) {
            // Handle generic exceptions
            Map<String, String> responseObj = new HashMap<>();
            responseObj.put("error", e.getMessage());
            builder = Response.status(Response.Status.BAD_REQUEST).entity(responseObj);
        }

        return builder.build();
    }

    
    private void validateMember(Member member) throws ConstraintViolationException, ValidationException {
        Set<ConstraintViolation<Member>> violations = validator.validate(member);

        if (!violations.isEmpty()) {
            throw new ConstraintViolationException(new HashSet<ConstraintViolation<?>>(violations));
        }

        if (emailAlreadyExists(member.getEmail())) {
            throw new ValidationException("Unique Email Violation");
        }
    }

    
    private Response.ResponseBuilder createViolationResponse(Set<ConstraintViolation<?>> violations) {

        Map<String, String> responseObj = new HashMap<>();

        for (ConstraintViolation<?> violation : violations) {
            responseObj.put(violation.getPropertyPath().toString(), violation.getMessage());
        }

        return Response.status(Response.Status.BAD_REQUEST).entity(responseObj);
    }

    
    public boolean emailAlreadyExists(String email) {
        Member member = null;
        try {
            member = repository.findByEmail(email);
        } catch (NoResultException ignore) {
            // ignore
        }
        return member != null;
    }
}

@Path("/members") でこのリソースへの基底パスを指定します。ルートURLと合わせて/rest/members のURLでこのクラスのリソースにアクセスできるようになります。

@GET@POSTメソッド指定して、@Produces(MediaType.APPLICATION_JSON)JSON形式でレスポンス返すように指定しています。 下部は単なるヘルパメソッドです。

Member.java の修正

先ほどのサービスからは JSON 形式で Member を直接返していました。この Member を JSONシリアライズできるように @XmlRootElement を追加します。

package etc9.model;

import java.io.Serializable;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.persistence.UniqueConstraint;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;
import javax.xml.bind.annotation.XmlRootElement;

@SuppressWarnings("serial")
@Entity
@XmlRootElement
@Table(uniqueConstraints = @UniqueConstraint(columnNames = "email"))
public class Member implements Serializable {
    ・・・
}

前回のものに @XmlRootElement を追加しただけです。

index.xhtml の修正

以上で動くのですが、画面にリンクを追加しておきます。

<!-- 特定のMemberの取得 -->
<a href="#{request.contextPath}/rest/members/#{_member.id}">/rest/members/#{_member.id}</a>

<!-- Memberのリスト取得 -->
<a href="#{request.contextPath}/rest/members">/rest/members</a>

最下部に追加します。index.xhtml の全体は以下のようになります。

<?xml version="1.0" encoding="UTF-8"?>
<ui:composition xmlns="http://www.w3.org/1999/xhtml"
                xmlns:ui="http://java.sun.com/jsf/facelets"
                xmlns:f="http://java.sun.com/jsf/core"
                xmlns:h="http://java.sun.com/jsf/html"
                template="/WEB-INF/templates/default.xhtml">
    <ui:define name="content">

        <h:form id="reg">
            <h2>Member Registration</h2>
            <h:panelGrid columns="3" columnClasses="titleCell">
                <h:outputLabel for="name" value="Name:" />
                <h:inputText id="name" value="#{newMember.name}" />
                <h:message for="name" errorClass="invalid" />

                <h:outputLabel for="email" value="Email:" />
                <h:inputText id="email" value="#{newMember.email}" />
                <h:message for="email" errorClass="invalid" />
            </h:panelGrid>
            <p>
                <h:panelGrid columns="2">
                    <h:commandButton id="register"
                                     action="#{memberController.register}"
                                     value="Register" styleClass="btn btn-primary" />
                    <h:messages styleClass="messages"
                                errorClass="invalid" infoClass="valid"
                                warnClass="warning" globalOnly="true" />
                </h:panelGrid>
            </p>
        </h:form>
        
        <hr/>
        <h2>Members</h2>
        <h:panelGroup rendered="#{empty members}">
            <em>No registered members.</em>
        </h:panelGroup>
        <h:dataTable var="_member" value="#{members}" rendered="#{not empty members}" 
                     styleClass="table table-striped table-bordered">
            <h:column>
                <f:facet name="header">Id</f:facet>
                #{_member.id}
            </h:column>
            <h:column>
                <f:facet name="header">Name</f:facet>
                #{_member.name}
            </h:column>
            <h:column>
                <f:facet name="header">Email</f:facet>
                #{_member.email}
            </h:column>
            <h:column>
                <f:facet name="header">REST URL</f:facet>
                <a href="#{request.contextPath}/rest/members/#{_member.id}">/rest/members/#{_member.id}</a>
            </h:column>
            <f:facet name="footer">
                REST URL for all members: <a href="#{request.contextPath}/rest/members">/rest/members</a>
            </f:facet>
        </h:dataTable>
    </ui:define>
</ui:composition>

実行

前回と同様に

./gradlew war cargoRunLocal -i

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

http://localhost:8080/example/index.jsf

登録 Member にリンクが出ます。

f:id:Naotsugu:20150215230610p:plain

Member のリンク

f:id:Naotsugu:20150215230621p:plain

下部の一覧リンク

f:id:Naotsugu:20150215230628p:plain

JSON 形式でリソース取得ができていますね。

最後に Arquillian のテストを書いておきましょう。次回 分速で始める JavaEE 7 〜 Arquillian 〜 - A Memorandum