前回
の続き。
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 にリンクが出ます。
Member のリンク
下部の一覧リンク
JSON 形式でリソース取得ができていますね。
最後に Arquillian のテストを書いておきましょう。次回