コンピュータクワガタ

かっぱのかっぱによるコンピュータ関連のサイトです

クラウド活用のためのAndroid業務アプリ開発入門のコードを改変してみたよ 改造編

前置き

クラウド活用のためのAndroid業務アプリ開発入門

クラウド活用のためのAndroid業務アプリ開発入門


の本を用いてクラウド+Androidアプリの勉強会をすることになったため、その予習を兼ねての記録。
3章のコードをGAEの勉強を兼ねて変更した。
また、書籍管理アプリを作る都合、書籍という言葉を使った場合には書籍管理の書籍とする。また、「クラウド活用のためのAndroid業務アプリ開発入門」は本とする。
自身がJavaの世界でWebアプリばかり作ってきたため、この本のサンプルだとやや勉強会には適さない部分があると感じたため、本のサンプルを本の範囲から逸脱しない程度に改変した。
実際の業務アプリにするためには、エラー処理、また不正なアクセスに対する処理等不十分であるが、それを行うと本質の理解が難しくなるため、それは次の機会とする。
まずは、書籍のサンプルをもう少しJavaらしく、また勉強会参加者にとって少しでも学習になるコードになるように変更してみた。
コードの詳しい説明は実際の本を手にとって確認して頂くとして、ここでは変更したコードの説明に注力したい。
また、RESTっぽく動作するようにした。初学者に近い人を想定しているためそのほうが動作がわかりやすいと感じるためである。

変更したサンプル

パッケージ構成は以下。

本のサンプルから変更していないコードは掲載しない。本を確認して頂きたい。
まず、PMFクラス。これは、GoogleのドキュメントにもあるようにPersitenceManagerFactoryのインスタンスは使いまわせるため、シングルトンとして取得する。実際にはPersistenceManagerを取得するのでその取得のメソッドgetPersistenceManagerを追加した。

package com.example.gae.bookshelf;

import javax.jdo.JDOHelper;
import javax.jdo.PersistenceManager;
import javax.jdo.PersistenceManagerFactory;

public class PMF {
    private static final PersistenceManagerFactory pmfInstance = JDOHelper.getPersistenceManagerFactory("transactions-optional");
    
    private PMF() {};
    
    public static PersistenceManagerFactory get() {
        return pmfInstance;
    }
    
    public static PersistenceManager getPersistenceManager() {
        return pmfInstance.getPersistenceManager();
    }
}

次にデータアクセス部分について共通する部分が多いため、Daoクラスにまとめた。単純にまとめただけでエラー処理等不十分なところはそのままにしてある。この本を読めば内容は簡単にわかるため説明は割愛する。エラー処理に関してはBigTable自体に不慣れなところもあるためまたの機会にする。

package com.example.gae.bookshelf;

import java.util.List;

import javax.jdo.PersistenceManager;
import javax.jdo.Query;

public class BookDao {
    /**
     * Bookオブジェクトの格納
     * @param book 格納するオブジェクト
     */
    public static void create(Book book) {
        PersistenceManager pm = PMF.getPersistenceManager();
        try {
            pm.makePersistent(book);
        } finally {
            pm.close();
        }
    }

    /**
     * Bookオブジェクトの格納。オブジェクトはメソッド中で作成する。
     * @param title
     * @param author
     * @param publisher
     */
    public static void create(String title, String author, String publisher) {
        create(new Book(title, author, publisher));
    }

    /**
     * Bookの更新。
     * @param bookId 更新するBookのキー
     * @param book 更新する内容
     */
    public static void update(Long bookId, Book book) {
        PersistenceManager pm = PMF.getPersistenceManager();

        try {
            List<Book> results = searchByKey(pm, bookId);

            Book updateBook = results.get(0);
            updateBook.setTitle(book.getTitle());
            updateBook.setAuthor(book.getAuthor());
            updateBook.setPublisher(book.getPublisher());
        } finally {
            pm.close();
        }
    }

    /**
     * Bookの更新。
     * @param bookId 更新するBookのキー
     * @param title
     * @param author
     * @param publisher
     */
    public static void update(Long bookId, String title, String author,
            String publisher) {
        update(bookId, new Book(title, author, publisher));
    }

    /**
     * Bookの削除
     * @param bookId 削除するBookのキー
     */
    public static void delete(Long bookId) {
        PersistenceManager pm = PMF.getPersistenceManager();

        try {
            pm.deletePersistentAll(searchByKey(pm, bookId));
        } finally {
            pm.close();
        }
    }

    /**
     * 登録されているBookのListの取得
     * @return 登録されているBookのList
     */
    @SuppressWarnings("unchecked")
    public static List<Book> selectAll() {
        PersistenceManager pm = PMF.getPersistenceManager();

        Query query = pm.newQuery(Book.class);
        return (List<Book>) query.execute();
    }

    /**
     * キーに対応するBookオブジェクトの取得
     * @param bookId 検索キー
     * @return キーに対応するBookオブジェクト
     */
    public static Book searchByKey(Long bookId) {
        List<Book> bookList = searchByKey(PMF.getPersistenceManager(), bookId);

        return bookList.get(0);
    }

    @SuppressWarnings("unchecked")
    private static List<Book> searchByKey(PersistenceManager pm, Long bookId) {
        Query query = pm.newQuery(Book.class);
        query.setFilter("id==pBookId");
        query.declareParameters("long pBookId");

        return (List<Book>) query.execute(bookId);
    }
}

Servletは大きく変更した部分である。本ではJSPファイルにリダイレクトで画面遷移していたがJSPへの直接アクセスはやめWEB-INF/jspフォルダ以下にファイルを置き、フォワードで遷移するようにした。また、BookshelfServletは/action/*でアクセスするように変更した。そのため、web.xmlを以下のように変更している。

<?xml version="1.0" encoding="utf-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" version="2.5">
    <servlet>
        <servlet-name>Bookshelf</servlet-name>
        <servlet-class>com.example.gae.bookshelf.BookshelfServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>Bookshelf</servlet-name>
        <url-pattern>/action/*</url-pattern>
    </servlet-mapping>
    <welcome-file-list>
        <welcome-file>index.html</welcome-file>
    </welcome-file-list>
</web-app>

合わせて、index.htmlは/actionに遷移するように変更した。

<!DOCTYPE html>
<html>
 <head>
  <meta charset="UTF-8">
  <meta http-equiv="refresh" content="0;URL=/action">
  <title>書籍管理</title>
 </head>
 <body>
 </body>
</html>

BookshelfServletは、基本的にコントローラの役割に徹するように変更した。リクエストURIを解析し、適切なアクションを呼び出し、画面遷移するという流れにした。

package com.example.gae.bookshelf;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.example.gae.bookshelf.action.Action;
import com.example.gae.bookshelf.action.DeleteAction;
import com.example.gae.bookshelf.action.JsonAction;
import com.example.gae.bookshelf.action.NewAction;
import com.example.gae.bookshelf.action.RegistAction;
import com.example.gae.bookshelf.action.UpdateAction;
import com.example.gae.bookshelf.action.UpdateRequestAction;

@SuppressWarnings("serial")
public class BookshelfServlet extends HttpServlet {
    /** 更新、削除のキー */
    public static final String BOOK_ID = "bookId";

    /** actionごとに実行するクラスを指定 */
    private static final Map<String, Class<? extends Action>> ACTION_MAP;

    static {
        ACTION_MAP = new HashMap<String, Class<? extends Action>>();
        ACTION_MAP.put("new", NewAction.class);
        ACTION_MAP.put("update", UpdateAction.class);
        ACTION_MAP.put("update_request", UpdateRequestAction.class);
        ACTION_MAP.put("delete", DeleteAction.class);
        ACTION_MAP.put("regist", RegistAction.class);
        ACTION_MAP.put("json", JsonAction.class);
    }

    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        String[] params = request.getRequestURI().split("/");

        String forwardPage = "/WEB-INF/jsp/index.jsp";
        if (params.length >= 3) {
            try {
                Action action = ACTION_MAP.get(params[2]).newInstance();
                forwardPage = action.execute(request, response);
                if (forwardPage == null) {
                    return;
                }
            } catch (InstantiationException e) {
            } catch (IllegalAccessException e) {
            }
        }

        request.getRequestDispatcher(forwardPage).forward(request, response);
    }

    @Override
    protected void doPost(HttpServletRequest request,
            HttpServletResponse response) throws ServletException, IOException {
        String[] params = request.getRequestURI().split("/");

        String forwardPage = "/WEB-INF/jsp/error.jsp";
        try {
            Action action = ACTION_MAP.get(params[2]).newInstance();
            forwardPage = action.execute(request, response);
        } catch (InstantiationException e) {
        } catch (IllegalAccessException e) {
        }

        request.getRequestDispatcher(forwardPage).forward(request, response);
    }
}

本ではGETメソッドとPOSTメソッドが分かれていたのでそのまま使用した。図で画面遷移を確認すると以下の通り。

まず、GETメソッドの処理。

  • /action/jsonのリクエストはJsonAction#executeを実行
  • /action/registのリクエストはRegistAction#executeを実行
  • それ以外のactionは、/WEB-INF/jsp/index.jspにフォワード

次に、POSTメソッドの処理。

  • /action/newのリクエストはNewAction#executeを実行
  • /action/updateのリクエストはUpdateAction#executeを実行
  • /action/update_requestのリクエストはUpdateRequestAction#executeを実行
  • /action/deleteのリクエストはDeleteAction#executeを実

GETのそれ以外のactionの場合を確認する。index.jspは以下のようしている。本ではカスタムタグやELは使用されていなかったので、同様にスクリプトレットで記載している。内容的には本と同様に登録されている書籍の一覧を表示している。index.jspのactionは以下となる。

  • 新規登録のアンカーで、/action/registをGETでリクエストする。
  • 更新ボタンはそれぞれ「/action/update_request/書籍のID」をPOSTする。URIに含まれる書籍のIDを更新するアクションとなる。
  • 削除ボタンはそれぞれ「/action/delete/書籍のID」をPOSTする。URIに含まれる書籍のIDを削除する。
<%@ page language="java"
    import="com.google.appengine.api.users.*"
    import="javax.jdo.*"
    import="java.util.*"
    import="com.example.gae.bookshelf.*"
    
    contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"
%>
<%
	List<Book> results = BookDao.selectAll();
%>
<!DOCTYPE html>
<html>
 <head>
  <meta charset="UTF-8">
  <title>書籍管理</title>
 </head>
 <body>
  <a href="/action/regist">書籍登録</a>
  
  <% if (results.size() > 0) { %>
  <table class="sample1">
   <thead>
    <th>タイトル</th>
    <th>著者</th>
    <th>出版社</th>
    <th></th>
    <th></th>
   </thead>
   <tbody>
    <% for (Book book : results) { %>
     <tr>
      <td><%= book.getTitle() %></td>
      <td><%= book.getAuthor() %></td>
      <td><%= book.getPublisher() %></td>
      <td><form action="/action/update_request/<%= book.getId() %>" method="post"><input type="submit" value="更新"></form></td>
      <td><form action="/action/delete/<%= book.getId() %>" method="post"><input type="submit" value="削除"></form></td>
     </tr>
    <% } %>
   </tbody>
  </table>
  <% } else {%>
   書籍が登録されていません。
  <% } %>
 </body>
</html>

ここからは実際のactionを見ていく。actionはInterface Actionを実装している。そのActionはexecuteメソッドのみ定義している。戻り値は遷移先のJSPのパスである。

package com.example.gae.bookshelf.action;

import java.io.IOException;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public interface Action {
    String execute(HttpServletRequest request, HttpServletResponse response)
            throws IOException;
}
/action/regist

/action/registはGETでリクエストされ新規登録画面を表示する。/action/registはRegistAction#executeで処理される。

package com.example.gae.bookshelf.action;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class RegistAction implements Action {
    @Override
    public String execute(HttpServletRequest request,
            HttpServletResponse response) {
        return "/WEB-INF/jsp/regist.jsp";
    }
}
/action/update_request

/action/update_requestはPOSTでリクエストされ、選択した書籍の更新画面を表示する。画面上でクリックされた本のIDはURIからわかるようになっている。たとえば、「/action/update_request/5」というリクエストがあれば、bookIdが5の書籍を更新する。本の例だとGETパラメータで処理をしていたが、この例ではrequest#setAttributeでbookIdをJSPに渡している。

package com.example.gae.bookshelf.action;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.example.gae.bookshelf.BookshelfServlet;

public class UpdateRequestAction implements Action {
    @Override
    public String execute(HttpServletRequest request,
            HttpServletResponse response) {
        Long bookId = Long.parseLong(request.getRequestURI().split("/")[3]);
        request.setAttribute(BookshelfServlet.BOOK_ID, bookId);

        return "/WEB-INF/jsp/regist.jsp";
    }
}

ここで、/action/registや、/action/update_requestの遷移先であるregist.jspの中身を確認する。本と違うのはモードの判別にbookIdがrequestパラメータとしてわたってきているかどうかで判別しているところである。getAttributeでbookIdを取得し、値が存在すればそのbookIdの書籍の更新モードとし、なければ書籍の新規登録モードとしている。モードの違いは、タイトル〜出版社に初期値をセットするかどうかと、formのaction属性の値が異なる。

<%@ page language="java"
    import="java.util.*"
    import="com.example.gae.bookshelf.*"
    
    contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"
%>
<%
    long bookId = -1;
    String buttonLabel = "登録する";
    String action = "new";
    Book book = new Book("", "", "");
    
    if (request.getAttribute(BookshelfServlet.BOOK_ID) != null) {
        bookId = (Long) request.getAttribute(BookshelfServlet.BOOK_ID);
    }
    if (bookId != -1) {
        book = BookDao.searchByKey(bookId);
        action = "update/" + bookId;
        buttonLabel = "更新する";
    }
%>
<!DOCTYPE html>
<html>
 <head>
  <meta charset="UTF-8">
  <link href="/css/default.css" rel="stylesheet" type="text/css">
  <title>書籍登録</title>
 </head>
 <body>
  <form action="/action/<%= action %>" method="post">
 タイトル<input type="text" name="title" value="<%= book.getTitle() %>"><br>
 著者<input type="text" name="author" value="<%= book.getAuthor() %>"><br>
 出版社<input type="text" name="publisher" value="<%= book.getPublisher() %>"><br>
 <input type="submit" value="<%= buttonLabel %>">
  </form>
 </body>
</html>
/action/delete

/action/deleteはPOSTでリクエストされ、選択された書籍の情報を削除する。更新と同様にURIから削除対象のbookIdを取得する。実際の削除はDaoに依頼するだけであり、非常に簡潔に書ける。

package com.example.gae.bookshelf.action;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.example.gae.bookshelf.BookDao;

public class DeleteAction implements Action {
    @Override
    public String execute(HttpServletRequest request,
            HttpServletResponse response) {
        Long bookId = Long.parseLong(request.getRequestURI().split("/")[3]);
        BookDao.delete(bookId);
        return "/WEB-INF/jsp/index.jsp";
    }
}
/action/new

/action/newはregist.jspからPOSTでリクエストされ、入力された情報を登録する。

package com.example.gae.bookshelf.action;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.example.gae.bookshelf.BookDao;

public class NewAction implements Action {
    @Override
    public String execute(HttpServletRequest request,
            HttpServletResponse response) {
        String title = request.getParameter("title");
        String author = request.getParameter("author");
        String publisher = request.getParameter("publisher");

        BookDao.create(title, author, publisher);
        return "/WEB-INF/jsp/index.jsp";
    }
}
/action/update

/action/updateは、regist.jspからPOSTでリクエストされ、指定のbookIdの書籍を入力された情報で更新する。

package com.example.gae.bookshelf.action;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.example.gae.bookshelf.BookDao;

public class UpdateAction implements Action {
    @Override
    public String execute(HttpServletRequest request,
            HttpServletResponse response) {
        String title = request.getParameter("title");
        String author = request.getParameter("author");
        String publisher = request.getParameter("publisher");

        Long bookId = Long.parseLong(request.getRequestURI().split("/")[3]);
        BookDao.update(bookId, title, author, publisher);
        return "/WEB-INF/jsp/index.jsp";
    }
}
/action/json

/action/jsonはGETでリクエストされ、書籍の情報をJSON形式で返す。

package com.example.gae.bookshelf.action;

import java.io.IOException;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.example.gae.bookshelf.BookDao;
import com.google.gson.Gson;

public class JsonAction implements Action {
    @Override
    public String execute(HttpServletRequest request,
            HttpServletResponse response) throws IOException {
        Gson gson = new Gson();
        String jsonstr = gson.toJson(BookDao.selectAll());

        response.setContentType("application/json; charset=UTF-8");
        response.getWriter().println(jsonstr);

        return null;
    }
}

感想

普段RDBを使っているため、BigTableだとどうすればいいのかという点がまだはっきりしない。慣れるためにも検索処理等を追加してみる。また、JSTLを使いスクリプトレットを使わない形にしてみる。BigTable自体は

オープンソース徹底活用 Slim3 on Google App Engine for Java

オープンソース徹底活用 Slim3 on Google App Engine for Java


が本屋で流し読みした感じだと詳しく書かれていてよさそうだったので、今度買ってくる。