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

JSONIC + Slim3 + GAE/JでRESTサービスを構築する方法 Part4

これまでの記事はこちら。

今回は、検索機能や登録機能のビジネスロジック部分を解説。
まず、実装に取り掛かる前に、Eclipseプロジェクトに設定を追加。プロジェクトのルートに、libフォルダを作成して、そこにslim3-gen-1.0.14.jarを追加。slim3-gen-1.0.14.jarは、Google Code Archive - Long-term storage for Google Code Project Hosting.からslim3-blank-1.0.14.zipをダウンロードしてきて解凍すると、libフォルダの中に入ってます。
次に、Eclipseプロジェクトのプロパティから、Java Compiler > Annotation Processingを選択して、Generated source directoryに「target/apt_generated」を入力。

そのままAnnotation Processing > Factory Pathを選択して、Add JARsボタンから最初に追加したslim3-gen-1.0.14.jarを追加。

最後に、Java Build Pathを選択して、target/apt_generatedフォルダをソースフォルダとして追加。

これで、Eclipseプロジェクトの設定は終わり。もし、target/apt_generatedができていなかったら、ビルドしなおしてみるといいかも。

では、ビジネスロジックの実装へ。まずは、com.example.logicパッケージを作成して、そこにGreetingLogicインターフェースを作成。

package com.example.logic;

import java.util.List;
import java.util.Map;

import com.example.model.Messages;

/**
 * @author jupitris
 * 
 */
public interface GreetingLogic {

    /**
     * Returns the list of message which is found by condition.
     * 
     * @param param
     *            the condition for finding data.
     * @return the list of message
     */
    List<Messages> find(Map<String, Object> param);

    /**
     * Create a message to data store.
     * 
     * @param messages
     *            the <code>Messages</code> which create to data store.
     * @return the <code>Messages</code> if created to data store.
     */
    Messages create(Messages messages);

    /**
     * Update a message to data store.
     * 
     * @param messages
     *            the <code>Messages</code> which update to data store.
     * @return the <code>Messages</code> if updated to data store.
     */
    Messages update(Messages messages);

    /**
     * Delete a message from data store.
     * 
     * @param messages
     *            the <code>Messages</code> which delete from data store.
     */
    void delete(Messages messages);
}

次は、さっき作成したインターフェースの具象クラスを作成。com.example.logic.implパッケージを作成して、そこにGreetingLogicImplクラスを作成。

package com.example.logic.impl;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import org.slim3.datastore.Datastore;
import org.slim3.datastore.FilterCriterion;
import org.slim3.datastore.ModelQuery;
import org.slim3.util.BeanUtil;

import com.example.logic.GreetingLogic;
import com.example.meta.MessagesMeta;
import com.example.model.Messages;
import com.google.appengine.api.datastore.Transaction;

/**
 * @author jupitris
 * 
 */
public class GreetingLogicImpl implements GreetingLogic {

    /** meta data. */
    private final MessagesMeta meta = MessagesMeta.get();

    /*
     * (non-Javadoc)
     * 
     * @see com.example.logic.GreetingLogic#find(java.util.Map)
     */
    @Override
    public List<Messages> find(Map<String, Object> param) {
        ModelQuery<Messages> query = Datastore.query(meta);

        // create filters. these filters become "where" statement.
        List<FilterCriterion> filter = new ArrayList<FilterCriterion>();
        for (Map.Entry<String, Object> entry : param.entrySet()) {
            if (entry.getKey().equals(meta.message.getName())) {
                filter.add(meta.message.equal((String) entry.getValue()));
            }
        }
        FilterCriterion[] filters = filter.toArray(new FilterCriterion[filter.size()]);

        if (filters.length > 0) {
            query = query.filter(filters);
        }
        return query.asList();
    }

    /*
     * (non-Javadoc)
     * 
     * @see com.example.logic.GreetingLogic#create(com.example.model.Messages)
     */
    @Override
    public Messages create(Messages messages) {
        Transaction tx = Datastore.beginTransaction();
        Datastore.put(messages);
        tx.commit();
        return messages;
    }

    /*
     * (non-Javadoc)
     * 
     * @see com.example.logic.GreetingLogic#update(com.example.model.Messages)
     */
    @Override
    public Messages update(Messages messages) {
        Transaction tx = Datastore.beginTransaction();
        Messages latest = Datastore.get(meta, messages.getKey(), messages.getVersion());
        BeanUtil.copy(messages, latest);
        Datastore.put(tx, latest);
        tx.commit();
        return latest;
    }

    /*
     * (non-Javadoc)
     * 
     * @see com.example.logic.GreetingLogic#delete(com.example.model.Messages)
     */
    @Override
    public void delete(Messages messages) {
        Transaction tx = Datastore.beginTransaction();
        Messages latest = Datastore.get(meta, messages.getKey(), messages.getVersion());
        Datastore.delete(latest.getKey());
        tx.commit();
    }

}

最後に、GreetingControllerクラスを修正。作成したビジネスロジックを呼び出す処理を追加。

package com.example.controller;

import java.util.List;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.example.logic.GreetingLogic;
import com.example.logic.impl.GreetingLogicImpl;
import com.example.model.Messages;

/**
 * @author jupitris
 * 
 */
public class GreetingController {

    /** logger. */
    private static Logger logger = LoggerFactory.getLogger(GreetingController.class);

    private final GreetingLogic logic = new GreetingLogicImpl();

    /**
     * Returns the list of <code>Messages</code> which is found by condition.
     * 
     * @param param
     *            the condition for finding data.
     * @return the list of <code>Messages</code>
     */
    public List<Messages> find(Map<String, Object> param) {
        logger.debug("find was called.");
        return logic.find(param);
    }

    /**
     * Create a message to data store.
     * 
     * @param messages
     *            the <code>Messages</code> which create to data store.
     */
    public void create(Messages messages) {
        logger.debug("create was called.");
        logic.create(messages);
    }

    /**
     * Update a message to data store.
     * 
     * @param messages
     *            the <code>Messages</code> which update to data store.
     */
    public void update(Messages messages) {
        logger.debug("update was called.");
        logic.update(messages);
    }

    /**
     * Delete a message from data store.
     * 
     * @param messages
     *            the <code>Messages</code> which delete from data store.
     */
    public void delete(Messages messages) {
        logger.debug("delete was called.");
        logic.delete(messages);
    }
}

これで、簡単ながらもひと通りの処理は実装完了。それぞれの処理に関する詳しい話は、また別の記事にて。一応、最低限のテストコードも載せておきます。

package com.example.logic.impl;

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

import junit.framework.Assert;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.slim3.tester.AppEngineTestCase;

import com.example.logic.GreetingLogic;
import com.example.model.Messages;

/**
 * @author jupitris
 * 
 */
public class GreetingLogicImplTest extends AppEngineTestCase {

    @Override
    @Before
    public void setUp() throws Exception {
        super.setUp();
    }

    @Override
    @After
    public void tearDown() throws Exception {
        super.tearDown();
    }

    @Test
    public void testCreate01() {
        Messages expected = new Messages();
        expected.setMessage("test create 01");

        GreetingLogic logic = new GreetingLogicImpl();

        Messages actual = logic.create(expected);

        Assert.assertNotNull(actual);
        Assert.assertEquals(expected.getMessage(), actual.getMessage());
        Assert.assertNotNull(actual.getKey());
        Assert.assertNotNull(actual.getVersion());
    }

    @Test
    public void testFind01() {
        Map<String, Object> param = new HashMap<String, Object>();
        param.put("message", "test find 01");

        Messages expected = new Messages();
        expected.setMessage("test find 01");

        GreetingLogic logic = new GreetingLogicImpl();

        logic.create(expected);
        List<Messages> actuals = logic.find(param);

        Assert.assertFalse(actuals.isEmpty());

        for (Messages actual : actuals) {
            Assert.assertEquals(expected.getMessage(), actual.getMessage());
        }
    }

    @Test
    public void testUpdate01() {
        Messages expected = new Messages();
        expected.setMessage("test update 01");

        GreetingLogic logic = new GreetingLogicImpl();

        Messages result = logic.create(expected);
        result.setMessage("test update 01-B");

        Messages actual = logic.update(result);

        Assert.assertNotNull(actual);
        Assert.assertEquals(expected.getMessage(), actual.getMessage());
        Assert.assertEquals(result.getMessage(), actual.getMessage());
        Assert.assertNotNull(actual.getKey());
        Assert.assertNotNull(actual.getVersion());
    }

    @Test
    public void testDelete01() {
        Map<String, Object> param = new HashMap<String, Object>();
        param.put("message", "test delete 01");

        Messages expected = new Messages();
        expected.setMessage("test delete 01");

        GreetingLogic logic = new GreetingLogicImpl();

        Messages result = logic.create(expected);
        logic.delete(result);

        List<Messages> actuals = logic.find(param);

        Assert.assertTrue(actuals.isEmpty());
    }
}

サービスの呼び出し方ですが、findとcreateだけ載せておきます。
まずはfindですが、HTTP MethodはGETにして、http://localhost:8080/restapp/greeting.jsonにアクセス。データが存在していれば、下記のようなJSONデータが返却されます。

[
	{
		"createdAt": 1326102940270,
		"createdBy": null,
		"key": {
			"complete": true,
			"id": 1,
			"kind": "Messages",
			"name": null,
			"namespace": "",
			"parent": null
		},
		"message": "Hello world!",
		"updatedAt": 1326102940270,
		"updatedBy": null,
		"version": 1
	},
	{
		"createdAt": 1326103005601,
		"createdBy": null,
		"key": {
			"complete": true,
			"id": 2,
			"kind": "Messages",
			"name": null,
			"namespace": "",
			"parent": null
		},
		"message": "こんにちは、世界!",
		"updatedAt": 1326103005601,
		"updatedBy": null,
		"version": 1
	}
]

続いてcreate。HTTP MethodはPOSTにして、BodyのContent-typeに「application/json」、Charasetに「UTF-8」、メッセージBody(っていう表現であってるかしら?)に「{"message" : "Hello world!"}」を設定して、http://localhost:8080/restapp/greeting.jsonにアクセス。処理が正しく動作すれば、データが登録されます。

次回は、APIの増やし方について。今はひとつのfindしか使えないけど、用途に応じて複数のfindメソッドが欲しくなるときありますよね。その定義の仕方について説明します。余裕があれば、他のこともふれるかも。