[from now] 2012/05/25 18:56:19

diff --git a/src/main/java/org/ryu22e/nico2cal/controller/myCalendar/SaveController.java b/src/main/java/org/ryu22e/nico2cal/controller/myCalendar/SaveController.java
index ac80e65..5cebc76 100644
--- a/src/main/java/org/ryu22e/nico2cal/controller/myCalendar/SaveController.java
+++ b/src/main/java/org/ryu22e/nico2cal/controller/myCalendar/SaveController.java
@@ -1,13 +1,17 @@
 package org.ryu22e.nico2cal.controller.myCalendar;

 import java.lang.reflect.Type;
+import java.util.Arrays;
 import java.util.HashMap;
 import java.util.Map;

 import org.ryu22e.nico2cal.controller.HttpStatusConstants;
+import org.ryu22e.nico2cal.model.MyCalendar;
 import org.ryu22e.nico2cal.service.CalendarService;
 import org.slim3.controller.Controller;
 import org.slim3.controller.Navigation;
+import org.slim3.controller.validator.Validators;
+import org.slim3.util.BeanUtil;

 import com.google.gson.Gson;
 import com.google.gson.reflect.TypeToken;
@@ -30,12 +34,17 @@ public final class SaveController extends Controller {
      */
     @Override
     public Navigation run() throws Exception {
-        String calendarId = request.getParameter("calendarId");
-        if (calendarId == null) {
+        Validators v = new Validators(request);
+        v.add("calendarId", v.required());
+        if (!v.validate()) {
             response.setStatus(HttpStatusConstants.FORBIDDEN);
             return null;
         }
-        calendarService.saveMyCalendar(calendarId);
+        MyCalendar myCalendar = new MyCalendar();
+        BeanUtil.copy(request, myCalendar);
+        myCalendar.setKeywords(Arrays.asList(request
+            .getParameterValues("keywords[]")));
+        calendarService.putMyCalendar(myCalendar);

         Map<String, String> map = new HashMap<String, String>();
         map.put("status", "ok");
diff --git a/src/main/java/org/ryu22e/nico2cal/model/MyCalendar.java b/src/main/java/org/ryu22e/nico2cal/model/MyCalendar.java
index dd1680d..cc13611 100644
--- a/src/main/java/org/ryu22e/nico2cal/model/MyCalendar.java
+++ b/src/main/java/org/ryu22e/nico2cal/model/MyCalendar.java
@@ -1,6 +1,7 @@
 package org.ryu22e.nico2cal.model;

 import java.io.Serializable;
+import java.util.List;

 import org.slim3.datastore.Attribute;
 import org.slim3.datastore.Model;
@@ -43,6 +44,11 @@ public final class MyCalendar implements Serializable {
     private User user;

     /**
+     *
+     */
+    private List<String> keywords;
+
+    /**
      * Returns the key.
      *
      * @return the key
@@ -95,6 +101,20 @@ public final class MyCalendar implements Serializable {
     }

     /**
+     * @return the keywords
+     */
+    public List<String> getKeywords() {
+        return keywords;
+    }
+
+    /**
+     * @param keywords the keywords to set
+     */
+    public void setKeywords(List<String> keywords) {
+        this.keywords = keywords;
+    }
+
+    /**
      * @return the user
      */
     public User getUser() {
diff --git a/src/main/java/org/ryu22e/nico2cal/service/CalendarService.java b/src/main/java/org/ryu22e/nico2cal/service/CalendarService.java
index 43c450d..6a45a48 100644
--- a/src/main/java/org/ryu22e/nico2cal/service/CalendarService.java
+++ b/src/main/java/org/ryu22e/nico2cal/service/CalendarService.java
@@ -32,6 +32,8 @@ import org.ryu22e.nico2cal.util.GoogleApiKeyUtil;
 import org.ryu22e.nico2cal.util.HtmlRemoveUtil;
 import org.slim3.datastore.Datastore;
 import org.slim3.datastore.ModelQuery;
+import org.slim3.util.BeanUtil;
+import org.slim3.util.CopyOptions;
 import org.slim3.util.DateUtil;
 import org.slim3.util.TimeZoneLocator;
 import org.xml.sax.SAXException;
@@ -259,13 +261,16 @@ public class CalendarService {

     /**
      * 連携対象のGoogle Calendarを登録する。
-     * @param calendarId
+     * @param myCalendar {@link MyCalendar}
      * @return 登録後のKey
-     * @throws NullPointerException パラメータがnullの場合。
+     * @throws NullPointerException パラメータmyCalendarまたは{@link MyCalendar#getCalendarId()}がnullの場合。
      * @throws AssertionError ログインしていない場合。
      */
-    public Key saveMyCalendar(String calendarId) {
-        if (calendarId == null) {
+    public Key putMyCalendar(MyCalendar myCalendar) {
+        if (myCalendar == null) {
+            throw new NullPointerException("myCalendar is null.");
+        }
+        if (myCalendar.getCalendarId() == null) {
             throw new NullPointerException("calendarId is null.");
         }
         User user = UserServiceFactory.getUserService().getCurrentUser();
@@ -273,17 +278,24 @@ public class CalendarService {
             throw new AssertionError("Require authorized.");
         }
         MyCalendarMeta mc = MyCalendarMeta.get();
-        MyCalendar myCalendar =
+        MyCalendar storedmyCalendar =
                 Datastore
                     .query(MyCalendar.class)
                     .filter(mc.user.equal(user))
                     .asSingle();
-        if (myCalendar == null) {
-            myCalendar = new MyCalendar();
-            myCalendar.setUser(user);
+        MyCalendar entity;
+        if (storedmyCalendar == null) {
+            // データストアにデータが存在しなければ、新規にデータを作る。
+            entity = myCalendar;
+            entity.setUser(user);
+        } else {
+            // データストアにデータが存在するなら、データを上書きする。
+            entity = storedmyCalendar;
+            CopyOptions options = new CopyOptions();
+            options.exclude("key", "user");
+            BeanUtil.copy(myCalendar, entity, options);
         }
-        myCalendar.setCalendarId(calendarId);
-        return Datastore.put(myCalendar);
+        return Datastore.put(entity);
     }

     /**
diff --git a/src/test/java/org/ryu22e/nico2cal/controller/myCalendar/SaveControllerTest.java b/src/test/java/org/ryu22e/nico2cal/controller/myCalendar/SaveControllerTest.java
index 47cbe26..9563f1e 100644
--- a/src/test/java/org/ryu22e/nico2cal/controller/myCalendar/SaveControllerTest.java
+++ b/src/test/java/org/ryu22e/nico2cal/controller/myCalendar/SaveControllerTest.java
@@ -2,6 +2,7 @@ package org.ryu22e.nico2cal.controller.myCalendar;

 import static org.hamcrest.CoreMatchers.*;
 import static org.junit.Assert.*;
+import static org.junit.matchers.JUnitMatchers.*;

 import java.lang.reflect.Type;
 import java.util.Map;
@@ -48,6 +49,9 @@ public final class SaveControllerTest extends ControllerTestCase {

         tester.request.setMethod("POST");
         tester.param("calendarId", "test1");
+        tester.paramValues(
+            "keywords[]",
+            new String[] { "keyword1", "keyword2" });
         tester.start("/myCalendar/save");
         SaveController controller = tester.getController();
         assertThat(controller, is(notNullValue()));
@@ -69,5 +73,7 @@ public final class SaveControllerTest extends ControllerTestCase {
                 Datastore.query(mc).filter(mc.user.equal(user)).asSingle();
         assertThat(myCalendar, is(notNullValue()));
         assertThat(myCalendar.getCalendarId(), is("test1"));
+        assertThat(myCalendar.getKeywords(), is(notNullValue()));
+        assertThat(myCalendar.getKeywords(), hasItems("keyword1", "keyword2"));
     }
 }
diff --git a/src/test/java/org/ryu22e/nico2cal/service/CalendarServiceTest.java b/src/test/java/org/ryu22e/nico2cal/service/CalendarServiceTest.java
index 8953d35..a161332 100644
--- a/src/test/java/org/ryu22e/nico2cal/service/CalendarServiceTest.java
+++ b/src/test/java/org/ryu22e/nico2cal/service/CalendarServiceTest.java
@@ -2,6 +2,7 @@ package org.ryu22e.nico2cal.service;

 import static org.hamcrest.CoreMatchers.*;
 import static org.junit.Assert.*;
+import static org.junit.matchers.JUnitMatchers.*;
 import static org.mockito.Mockito.*;

 import java.io.IOException;
@@ -314,7 +315,9 @@ public final class CalendarServiceTest extends AppEngineTestCase {
     public void 連携対象のGoogleCalendarを登録する_ログインしていない() throws Exception {
         assertThat(service, is(notNullValue()));

-        service.saveMyCalendar("test1");
+        MyCalendar myCalendar = new MyCalendar();
+        myCalendar.setCalendarId("test1");
+        service.putMyCalendar(myCalendar);
     }

     /**
@@ -325,11 +328,15 @@ public final class CalendarServiceTest extends AppEngineTestCase {
         assertThat(service, is(notNullValue()));

         tester.environment.setEmail("dummy@gmail.com");
-        Key key = service.saveMyCalendar("test1");
-        MyCalendar myCalendar = Datastore.getOrNull(MyCalendar.class, key);
-        assertThat(myCalendar, is(notNullValue()));
-        assertThat(myCalendar.getUser(), is(notNullValue()));
-        assertThat(myCalendar.getUser().getEmail(), is("dummy@gmail.com"));
+        MyCalendar myCalendar = new MyCalendar();
+        myCalendar.setCalendarId("test1");
+        myCalendar.setKeywords(Arrays.asList("keyword1", "keyword2"));
+        Key key = service.putMyCalendar(myCalendar);
+        MyCalendar storedMyCalendar =
+                Datastore.getOrNull(MyCalendar.class, key);
+        assertThat(storedMyCalendar, is(notNullValue()));
+        assertThat(storedMyCalendar.getUser(), is(notNullValue()));
+        assertThat(storedMyCalendar.getUser().getEmail(), is("dummy@gmail.com"));
     }

     /**
@@ -346,21 +353,45 @@ public final class CalendarServiceTest extends AppEngineTestCase {
         testData.setUser(UserServiceFactory.getUserService().getCurrentUser());
         Key testDataKey = Datastore.put(testData);

-        Key key = service.saveMyCalendar("test1");
-        MyCalendar myCalendar = Datastore.getOrNull(MyCalendar.class, key);
-        assertThat(myCalendar, is(notNullValue()));
-        assertThat(myCalendar.getKey(), is(testDataKey));
+        MyCalendar myCalendar = new MyCalendar();
+        myCalendar.setCalendarId("test2");
+        myCalendar.setKeywords(Arrays
+            .asList("keyword1", "keyword2", "keyword3"));
+        Key key = service.putMyCalendar(myCalendar);
+        MyCalendar storedMyCalendar =
+                Datastore.getOrNull(MyCalendar.class, key);
+        assertThat(storedMyCalendar, is(notNullValue()));
+        assertThat(storedMyCalendar.getKey(), is(testDataKey));
+        assertThat(storedMyCalendar.getCalendarId(), is("test2"));
+        assertThat(
+            storedMyCalendar.getKeywords(),
+            hasItems("keyword1", "keyword2", "keyword3"));
+    }
+
+    /**
+     * @throws Exception
+     */
+    @Test(expected = NullPointerException.class)
+    public void 連携対象のGoogleCalendarを登録する_パラメータmyCalendarがnull()
+            throws Exception {
+        assertThat(service, is(notNullValue()));
+
+        tester.environment.setEmail("dummy@gmail.com");
+        service.putMyCalendar(null);
     }

     /**
      * @throws Exception
      */
     @Test(expected = NullPointerException.class)
-    public void 連携対象のGoogleCalendarを登録する_パラメータがnull() throws Exception {
+    public void 連携対象のGoogleCalendarを登録する_パラメータcalendarIdがnull()
+            throws Exception {
         assertThat(service, is(notNullValue()));

         tester.environment.setEmail("dummy@gmail.com");
-        service.saveMyCalendar(null);
+        MyCalendar myCalendar = new MyCalendar();
+        myCalendar.setCalendarId(null);
+        service.putMyCalendar(myCalendar);
     }

     /**
diff --git a/war/WEB-INF/appengine-generated/datastore-indexes-auto.xml b/war/WEB-INF/appengine-generated/datastore-indexes-auto.xml
index 488e981..1316289 100644
--- a/war/WEB-INF/appengine-generated/datastore-indexes-auto.xml
+++ b/war/WEB-INF/appengine-generated/datastore-indexes-auto.xml
@@ -1,4 +1,4 @@
-<!-- Indices written at Wed, 23 May 2012 20:42:05 JST -->
+<!-- Indices written at Fri, 25 May 2012 17:48:17 JST -->

 <datastore-indexes/>

diff --git a/war/js/nico2ical.js b/war/js/nico2ical.js
index 759cb53..7640149 100644
--- a/war/js/nico2ical.js
+++ b/war/js/nico2ical.js
@@ -39,6 +39,7 @@ $(function () {

     var CalendarSummaries = function(selectedValue) {
         this.selectedValue = ko.observable(selectedValue);
+        this.keywords = ko.observable();
         this.isSaveSuccess = ko.observable(false);
         this.isSavaError = ko.observable(false);
         this.isDisconnectError = ko.observable(false);
@@ -57,7 +58,8 @@ $(function () {
                         dataType: 'json',
                         data: {
                             'csrftoken': $.cookie('csrftoken'),
-                            'calendarId': this.selectedValue()
+                            'calendarId': this.selectedValue(),
+                            'keywords': this.keywords().split(' ');
                             }
                     }
             ).success(function(data) {
diff --git a/war/myCalendar/index.jsp b/war/myCalendar/index.jsp
index 0dfbbc4..44774b5 100644
--- a/war/myCalendar/index.jsp
+++ b/war/myCalendar/index.jsp
@@ -13,8 +13,8 @@

       <ul class="nav nav-tabs" id="mycalendar-tab">
         <c:if test="${storedCredential}">
-          <li id="nav-calendar-select" class="active">
-            <a data-target="#mycalendar-select" href="#">連携先の選択</a>
+          <li id="nav-mycalendar-settings" class="active">
+            <a data-target="#mycalendar-settings" href="#">設定</a>
           </li>
           <li>
             <a data-target="#mycalendar-on-off" href="#">連携のON/OFF</a>
@@ -29,32 +29,51 @@

       <div class="tab-content">
         <c:if test="${storedCredential}">
-          <div class="tab-pane<c:if test="${storedCredential}"> active</c:if>" id="mycalendar-select">
+          <div class="tab-pane<c:if test="${storedCredential}"> active</c:if>" id="mycalendar-settings">
             <form class="well">
-              <div class="control-group">
-                <label class="control-label">連携するカレンダー名を選択して下さい（Google Calendar API利用上限の関係上、複数カレンダーの連携はできないようにしていますm(__)m）。</label>
-                <div class="controls">
-                  <c:if test="${calendars != null}">
-                    <c:forEach var="c" items="${calendars}">
-                      <label class="radio" for="calendarSummaries-${f:h(c.id)}">
-                        <input type="radio" id="calendarSummaries-${f:h(c.id)}" ${f:radio("calendarSummaries", c.id)} data-bind="checked: selectedValue" />
-                        ${f:h(c.summary)}
-                      </label>
-                    </c:forEach>
-                    <input id="save-calendar"
-                           class="btn"
-                           type="button"
-                           value="登録"
-                           data-loading-text="登録中..."
-                           data-complete-text="登録しました"
-                           data-error-text="登録に失敗しました"
-                           data-bind="css: { 'btn-primary': isEnableButton(), 'btn-success': isSaveSuccess(), 'btn-danger': isSavaError() }, enable: selectedValue().length > 0, click: updateCalendar"
-                            />
-                  </c:if>
-                </div><!-- /.controls -->
-              </div><!-- /.control-group -->
+              <fieldset>
+                <legend>連携先</legend>
+                <div class="control-group">
+                  <label class="control-label">連携するカレンダー名を選択して下さい（Google Calendar API利用上限の関係上、複数カレンダーの連携はできないようにしていますm(__)m）。</label>
+                  <div class="controls">
+                    <c:if test="${calendars != null}">
+                      <c:forEach var="c" items="${calendars}">
+                        <label class="radio" for="calendarSummaries-${f:h(c.id)}">
+                          <input type="radio" id="calendarSummaries-${f:h(c.id)}" ${f:radio("calendarSummaries", c.id)} data-bind="checked: selectedValue" />
+                          ${f:h(c.summary)}
+                        </label>
+                      </c:forEach>
+                    </c:if>
+                  </div><!-- /.controls -->
+                </div><!-- /.control-group -->
+              </fieldset>
+              <fieldset>
+                <legend>インポート条件</legend>
+                  <div class="control-group">
+                  <label class="control-label">タイトルまたは説明文に含まれるキーワード</label>
+                  <div class="controls">
+                    <input id="keywords"
+                           class="input-xxlarge"
+                           type="text"
+                           maxlength="50"
+                           data-bind="value: keywords"
+                           placeholder="スペースで区切ると複数のキーワードを指定できます。" />
+                  </div><!-- /.controls -->
+                </div><!-- /.control-group -->
+              </fieldset>
+              <div class="form-actions">
+                <input id="save-calendar"
+                       class="btn"
+                       type="button"
+                       value="登録"
+                       data-loading-text="登録中..."
+                       data-complete-text="登録しました"
+                       data-error-text="登録に失敗しました"
+                       data-bind="css: { 'btn-primary': isEnableButton(), 'btn-success': isSaveSuccess(), 'btn-danger': isSavaError() }, enable: selectedValue().length > 0, click: updateCalendar"
+                        />
+              </div>
             </form>
-          </div><!-- /#mycalendar-select -->
+          </div><!-- /#mycalendar-settings -->
         </c:if>
         <div class="tab-pane<c:if test="${!storedCredential}"> active</c:if>" id="mycalendar-on-off">
           <form class="well">